Compare commits

...

159 Commits

Author SHA1 Message Date
Derrick Hammer 039583d77e
*Update dist 2023-04-08 16:45:01 -04:00
Derrick Hammer a7e0fbd190
* Refactor `util.ts` to use `existing.ready` instead of `existing._channel.ready` and replace `existing.ready` with `rpc.ready`. 2023-04-08 16:43:56 -04:00
Derrick Hammer 7386b6de05
*Update dist 2023-04-08 15:02:22 -04:00
Derrick Hammer 35c67e1c92
*Bug fix 2023-04-08 15:02:11 -04:00
Derrick Hammer 05c9a7aea7
*Update dist 2023-04-08 14:38:29 -04:00
Derrick Hammer c787d9e757
*setupStream is now async 2023-04-08 14:37:51 -04:00
Derrick Hammer 8f7bd71e09
*Refactor setupStream, make it async, and await on ProtouxRPC ready when it exists in the browser 2023-04-08 14:37:13 -04:00
Derrick Hammer 788591b227
*update dist 2023-04-06 13:40:53 -04:00
Derrick Hammer 1d27cb5c67
*export all util functions 2023-04-06 13:40:25 -04:00
Derrick Hammer 93ed14843d
*Update dist 2023-03-29 17:25:45 -04:00
Derrick Hammer ab6f7d76e2
*If we fail to get the RPC methods list from the peer, abort the connection 2023-03-29 17:25:26 -04:00
Derrick Hammer 63ce23fa26
*Update dist 2023-03-25 11:26:38 -04:00
Derrick Hammer f9bf14d697
*Use maybeGetAsyncProperty 2023-03-25 11:26:22 -04:00
Derrick Hammer 6c6db87073
*Update dist 2023-03-25 10:51:42 -04:00
Derrick Hammer 1cc1a0d2b1
*Use maybeGetAsyncProperty to get remotePublicKey 2023-03-25 10:51:29 -04:00
Derrick Hammer bb36a80913
*Switch to using maybeGetAsyncProperty 2023-03-25 10:51:13 -04:00
Derrick Hammer 5e4617ac55
*add new helper maybeGetAsyncProperty to process the kernel client proxies if needed 2023-03-25 10:50:39 -04:00
Derrick Hammer 6323bc6b29
*Update dist 2023-03-25 09:42:53 -04:00
Derrick Hammer 6b9c865e12
*Handle web edge case where .dht returns an async function 2023-03-25 09:42:42 -04:00
Derrick Hammer 80ed28e8b4
*Update dist 2023-03-23 12:48:48 -04:00
Derrick Hammer 63270fb1da
*switch from relay-types to interface-relay 2023-03-23 12:48:31 -04:00
Derrick Hammer e74408b42a
*Update dist 2023-03-19 15:11:34 -04:00
Derrick Hammer 92fe42c37e
*Return the relay object, not the pubkey 2023-03-19 15:11:13 -04:00
Derrick Hammer 2f2bce84eb
*Update dist 2023-03-19 11:10:57 -04:00
Derrick Hammer 01b4a6517a
*Ensure RPC_PROTOCOL_SYMBOL is a string 2023-03-19 11:10:41 -04:00
Derrick Hammer 99f988addd
*Update dist 2023-03-19 10:35:54 -04:00
Derrick Hammer 0dc8eebc7e
*Add setupStream method from relay code to ensure only one RPC instance exists per socket 2023-03-19 10:35:36 -04:00
Derrick Hammer f875cd116f
*update dist 2023-03-18 14:49:36 -04:00
Derrick Hammer 065934461e
*use once to prevent memory leaks 2023-03-18 14:49:05 -04:00
Derrick Hammer e9e25b2573
*hook close event before querying 2023-03-18 14:48:43 -04:00
Derrick Hammer d572e39a65
*update dist 2023-03-18 12:27:49 -04:00
Derrick Hammer 52181dabcc
*remove unneded import 2023-03-18 12:27:32 -04:00
Derrick Hammer e417e00794
*make relay optional 2023-03-18 12:27:19 -04:00
Derrick Hammer d412a48f05
*update dist 2023-03-18 12:24:07 -04:00
Derrick Hammer cce09d1e95
*allow a buffer to be passed 2023-03-18 12:23:49 -04:00
Derrick Hammer ee5c1ea692
*update dist 2023-03-18 12:21:38 -04:00
Derrick Hammer 9bb3f2ab60
*If we are passed a buffer, convert it to hex 2023-03-18 12:21:14 -04:00
Derrick Hammer 1d66b2452f
*update dist 2023-03-18 12:15:29 -04:00
Derrick Hammer 0216b0d7ba
*update dist 2023-03-18 12:11:41 -04:00
Derrick Hammer 50f21f0434
*refactor rpc to use new swarm based p2p
*remove wisdom query for now
2023-03-18 12:11:23 -04:00
Derrick Hammer 62a1f9c05f
*Cleanup imports 2023-02-19 13:09:39 -05:00
Derrick Hammer 3868286416
*Cleanup imports 2023-02-19 13:05:05 -05:00
Derrick Hammer 7afe4cb0bb
*Cleanup imports 2023-02-19 13:04:41 -05:00
Derrick Hammer f0f2118c99
*Cleanup imports 2023-02-19 13:04:13 -05:00
Derrick Hammer 7f8dee6ad2
*Switch to pnpm 2023-02-19 13:03:38 -05:00
Derrick Hammer 0c51da781e
*Update dist 2023-01-06 01:19:07 -05:00
Derrick Hammer 5366f85c35
*Skip if relay has an error 2023-01-06 01:18:50 -05:00
Derrick Hammer be2531ec99
*Update dist 2022-12-04 07:19:13 -05:00
Derrick Hammer 5de486e680
*Handle both an error object and a rpc response object with conditional properties 2022-12-04 07:19:00 -05:00
Derrick Hammer bc34a95ebb
*Update dist 2022-12-04 06:37:24 -05:00
Derrick Hammer 8c2f857c55
*Add cache bypass support for simple query 2022-12-04 06:36:18 -05:00
Derrick Hammer 6429bd513c
*refactor query api to use object bags and a dedicated factory at a factory object namespace 2022-12-04 06:35:57 -05:00
Derrick Hammer 27d396d969
*Update dist 2022-12-04 05:39:59 -05:00
Derrick Hammer fc63e98557
*add support for bypassCache in wisdom query 2022-12-04 05:39:09 -05:00
Derrick Hammer 5e1c52352e
*Add new query type just for clearing a query hash 2022-12-04 05:38:46 -05:00
Derrick Hammer 144e19e635
*Add hashQuery utility function 2022-12-04 05:36:56 -05:00
Derrick Hammer 271a0c2911
*Refactor wisdom query to extract setupRelay to be a functional api 2022-12-04 05:35:52 -05:00
Derrick Hammer a8b0f36d16
*activeRelay not needed in network class 2022-12-04 02:46:25 -05:00
Derrick Hammer 960c321ecf
*Update dist 2022-12-04 02:42:04 -05:00
Derrick Hammer c986af2f4f
*Rename package to just rpc-client 2022-12-04 02:40:58 -05:00
Derrick Hammer 7263ecf907
*Epic refactor based on new RPC query design and protocol 2022-12-04 02:40:36 -05:00
Derrick Hammer 71eb37160c
*Update dist 2022-09-22 11:04:38 -04:00
Derrick Hammer 7a21a5069c
*Ensure we don't store an error inside the data property 2022-09-22 11:04:15 -04:00
Derrick Hammer 40cf1ff9e9
*Update dist 2022-09-22 09:37:54 -04:00
Derrick Hammer 6fa058d0a0
*should be >= 2022-09-22 09:37:36 -04:00
Derrick Hammer 7399d4aa35
*Update dist 2022-09-22 09:36:04 -04:00
Derrick Hammer 47356ed6e0
*If max relays is greater than 0, and we have more relays than max relays, pick a random selection 2022-09-22 09:35:15 -04:00
Derrick Hammer fa8910a4fd
*add maxRelays network option 2022-09-22 09:34:07 -04:00
Derrick Hammer a226c7deea
Revert "*Update dist"
This reverts commit b7978ddc0d.
2022-09-20 06:13:51 -04:00
Derrick Hammer 1d73d23700
Revert "*add a _timeoutCanceled property and abort handeTimeout if true"
This reverts commit 309b9b0ac6.
2022-09-20 06:13:38 -04:00
Derrick Hammer f9036e1c73
Revert "*Switch from clearing the timeout to setting the _timeoutCanceled flag"
This reverts commit b50f7036ff.
2022-09-20 06:13:37 -04:00
Derrick Hammer b7978ddc0d
*Update dist 2022-09-18 16:03:13 -04:00
Derrick Hammer b50f7036ff
*Switch from clearing the timeout to setting the _timeoutCanceled flag 2022-09-18 16:02:39 -04:00
Derrick Hammer 309b9b0ac6
*add a _timeoutCanceled property and abort handeTimeout if true 2022-09-18 16:01:51 -04:00
Derrick Hammer c841b45013
*Update dist 2022-09-09 22:25:58 -04:00
Derrick Hammer 4478eb3a23
*Remove unneeded return 2022-09-09 22:25:30 -04:00
Derrick Hammer 3d52e6fd7b
*Update dist 2022-09-09 22:13:47 -04:00
Derrick Hammer 88377a75ce
*If the error is a timeout, flag it to the resolve method 2022-09-09 22:05:22 -04:00
Derrick Hammer 62ef56b554
*Change how we check/clear the timer 2022-09-09 22:04:55 -04:00
Derrick Hammer 9a04c82a5b
*Update dist 2022-09-09 21:21:04 -04:00
Derrick Hammer 0eb1e33fce
*Clear query timer with relay timer 2022-09-09 21:20:37 -04:00
Derrick Hammer 3485a3f533
Change how we clear the timer 2022-09-09 21:18:02 -04:00
Derrick Hammer a3cb70fa24
*Update dist 2022-09-09 06:05:13 -04:00
Derrick Hammer 46f693a1f2
*Handle edge case if we have no responses 2022-09-09 06:04:02 -04:00
Derrick Hammer e6ec3cdc4c
*Update dist 2022-08-31 20:56:56 -04:00
Derrick Hammer e151647e8f
*If stream is canceled remove listener to prevent repeat cancel messages 2022-08-31 20:56:21 -04:00
Derrick Hammer f201005112
*Update dist 2022-08-31 20:45:09 -04:00
Derrick Hammer bba6fa89be
*If the stream is canceled send a message with a cancel property 2022-08-31 20:44:47 -04:00
Derrick Hammer 327c429d1e
*Update dist 2022-08-31 19:59:06 -04:00
Derrick Hammer dafe044e00
*Add support for canceling a streaming query and stop the data stream 2022-08-31 19:58:44 -04:00
Derrick Hammer 925bbba9e5
*Update dist 2022-08-31 19:35:23 -04:00
Derrick Hammer 26fd962090
*ensure passed streamHandler overrides options 2022-08-31 19:35:04 -04:00
Derrick Hammer 403297c255
*Update dist 2022-08-30 22:49:05 -04:00
Derrick Hammer a4f0f10a85
*Add bypassCache to simpleQuery 2022-08-30 22:48:46 -04:00
Derrick Hammer 70a6092079
*Update dist 2022-08-30 22:43:05 -04:00
Derrick Hammer ac8b1d4bf2
*Only close socket when stream is done 2022-08-30 22:42:43 -04:00
Derrick Hammer c750fce402
*Update dist 2022-08-28 23:19:45 -04:00
Derrick Hammer 4f855110f5
*Resolve should return a RPCResponse 2022-08-28 23:17:04 -04:00
Derrick Hammer bb0841cf4a
*Consolidate code to flatHash helper 2022-08-28 23:16:33 -04:00
Derrick Hammer 2785bdfa50
*Update dist 2022-08-28 22:18:04 -04:00
Derrick Hammer 951a7e3426
*Move to use @lumeweb/relay-types 2022-08-28 22:14:33 -04:00
Derrick Hammer ac53e5833a
*Update dist 2022-08-28 02:33:49 -04:00
Derrick Hammer 00ccbc9536
*Dont call init in constructor
*Rename init to run
*Make run a chained method and not async
2022-08-28 02:26:40 -04:00
Derrick Hammer b5bb0c1889
*Duplicate init call 2022-08-28 02:12:44 -04:00
Derrick Hammer ea2dc58838
*Missing typeof 2022-08-28 02:12:19 -04:00
Derrick Hammer a8121f0a39
*Update dist 2022-08-27 15:13:00 -04:00
Derrick Hammer e6e160c6ca
*Fix import 2022-08-27 15:12:42 -04:00
Derrick Hammer 11acd58320
*ERR_NOT_READY not needed 2022-08-27 15:11:01 -04:00
Derrick Hammer fb849550db
*Heavily refactor to use new RPC schema
*Create basic, wisdom, and streaming rpc request variants
2022-08-27 15:09:34 -04:00
Derrick Hammer f7a8b69a55
*Update dist 2022-08-18 19:20:23 -04:00
Derrick Hammer cc5c988ce7
*Add safety check on timer 2022-08-18 19:20:06 -04:00
Derrick Hammer b00e598c07
*Fix import 2022-08-18 19:17:41 -04:00
Derrick Hammer 801ae6cd4c
*add typecast 2022-08-18 19:17:32 -04:00
Derrick Hammer 152522e579
*Add error property to RPCResponse 2022-08-18 19:17:12 -04:00
Derrick Hammer d7b0c7d4de
*Update force to bypassCache 2022-08-18 19:10:07 -04:00
Derrick Hammer ef62888c31
*Update dist 2022-08-14 09:44:50 -04:00
Derrick Hammer 2a7fa853c9
*Add a max retry limit 2022-08-14 09:44:32 -04:00
Derrick Hammer 12db0806a8
*Update dist 2022-08-14 09:27:02 -04:00
Derrick Hammer 237e5796a8
*If object is null, default to empty object 2022-08-14 09:26:23 -04:00
Derrick Hammer 03a5933394 *Update dist 2022-07-31 23:11:31 -04:00
Derrick Hammer 681d3f4b9f *Ensure object is an array to handle both arrays and objects 2022-07-31 23:11:22 -04:00
Derrick Hammer 1174999864 *Add missing files 2022-07-31 23:02:03 -04:00
Derrick Hammer 9095cd5b5a *Update dist 2022-07-31 23:00:49 -04:00
Derrick Hammer 5cefe245af *Use custom json flatten algorithm to ensure deterministic hashing 2022-07-31 23:00:17 -04:00
Derrick Hammer 29271df627 *Update dist 2022-07-31 21:04:17 -04:00
Derrick Hammer 94c9cea074 *Update deps 2022-07-31 21:04:08 -04:00
Derrick Hammer eeffce5f02 *prettier 2022-07-31 21:03:35 -04:00
Derrick Hammer 8ecb7722b4 *Use json-stable-stringify to ensure deterministic json 2022-07-31 21:03:03 -04:00
Derrick Hammer cf3853702f *Use NodeJS.Timeout type 2022-07-31 21:02:27 -04:00
Derrick Hammer 8c95ddc07b *add typedef 2022-07-27 18:49:11 -04:00
Derrick Hammer ffbbc71c48 *Update dist 2022-07-26 21:25:47 -04:00
Derrick Hammer 9ad9010a96 *Promise property should be optional and cast 2022-07-26 21:25:38 -04:00
Derrick Hammer f14f32215c *Update dist 2022-07-26 20:59:27 -04:00
Derrick Hammer d3d94c7162 *Lazy load dht ready promise 2022-07-26 20:58:41 -04:00
Derrick Hammer efa8b50463 *Update dist 2022-07-25 21:58:53 -04:00
Derrick Hammer bce3f6d175 *Use js suffix 2022-07-25 21:58:44 -04:00
Derrick Hammer 33fc4e9190 *add typescript devdep 2022-07-25 21:58:22 -04:00
Derrick Hammer 2baf7a1eec *Update dist 2022-07-23 11:43:05 -04:00
Derrick Hammer bd4ac1584b *Add error file with const's for future use 2022-07-23 11:42:29 -04:00
Derrick Hammer 563173bd95 *Add a timeout for querying a relay with rejection 2022-07-23 11:41:58 -04:00
Derrick Hammer c7eede8435 *RPC/DHT protocol now uses a command before the data to separate it from other communications 2022-07-23 11:40:44 -04:00
Derrick Hammer cfc519b57b *add timeout setting for individual relay 2022-07-23 11:39:17 -04:00
Derrick Hammer 7d0bf7a538 *Update dist 2022-07-20 02:04:37 -04:00
Derrick Hammer 77f4d63186 *Add build command 2022-07-20 02:04:16 -04:00
Derrick Hammer 9a2f37a3b0 *rename promise to result 2022-07-19 20:30:07 -04:00
Derrick Hammer 79bf18a2ed *Update dist 2022-07-19 14:48:27 -04:00
Derrick Hammer 47daf47f45 *Add libskynet as dep 2022-07-19 14:47:56 -04:00
Derrick Hammer 42d53b6137 *Rwrite checkResponses 2022-07-19 14:47:33 -04:00
Derrick Hammer da06f787dc *DHT connect in queryRelay can be a promise 2022-07-19 14:46:10 -04:00
Derrick Hammer 1c18b041f3 *Move from require to import 2022-07-19 14:43:28 -04:00
Derrick Hammer d7ec437ce3 *Update dist 2022-06-27 16:26:13 -04:00
Derrick Hammer 5e3de30f51 *Add method to remove a relay and clear relays 2022-06-27 16:25:53 -04:00
Derrick Hammer e9bbdeac38 *Fix typo 2022-06-27 16:20:51 -04:00
Derrick Hammer cb4d3b9ca2 *Update dist 2022-06-27 15:56:56 -04:00
Derrick Hammer 091240fe3e *Reformat 2022-06-27 15:56:27 -04:00
Derrick Hammer 55a0684628 *add prettier 2022-06-27 15:55:48 -04:00
Derrick Hammer 5a4fa59650 *Update exports 2022-06-27 15:55:37 -04:00
Derrick Hammer e0d836f2ff *add dist 2022-06-27 15:51:51 -04:00
Derrick Hammer 289cd2f5cc *Initial version 2022-06-27 15:37:03 -04:00
40 changed files with 1359 additions and 3 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Lume Web
Copyright (c) 2022 Hammer Technologies LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,2 +1 @@
# dht-rpc-client
A client library that uses hypercore and the @lumeweb/relay server along with Skynet for web, to perform "Wisdom of the crowd" RPC requests.
# rpc-client

2
dist/error.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
export declare const ERR_INVALID_SIGNATURE = "INVALID_SIGNATURE";
//# sourceMappingURL=error.d.ts.map

1
dist/error.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,sBAAsB,CAAC"}

1
dist/error.js vendored Normal file
View File

@ -0,0 +1 @@
export const ERR_INVALID_SIGNATURE = "INVALID_SIGNATURE";

5
dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
export * from "./types.js";
export * from "./query/index.js";
export * from "./network.js";
export * from "./util.js";
//# sourceMappingURL=index.d.ts.map

1
dist/index.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}

4
dist/index.js vendored Normal file
View File

@ -0,0 +1,4 @@
export * from "./types.js";
export * from "./query/index.js";
export * from "./network.js";
export * from "./util.js";

34
dist/network.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
import RpcNetworkQueryFactory from "./query/index.js";
export default class RpcNetwork {
private _relaysAvailablePromise?;
private _relaysAvailableResolve?;
constructor(swarm?: any);
private _methods;
get methods(): Map<string, Set<string>>;
private _factory;
get factory(): RpcNetworkQueryFactory;
private _swarm;
get swarm(): any;
private _majorityThreshold;
get majorityThreshold(): number;
set majorityThreshold(value: number);
private _queryTimeout;
get queryTimeout(): number;
set queryTimeout(value: number);
private _relayTimeout;
get relayTimeout(): number;
set relayTimeout(value: number);
private _relays;
get relays(): Map<string, string[]>;
private _ready?;
get ready(): Promise<void>;
get readyWithRelays(): Promise<void>;
private _bypassCache;
get bypassCache(): boolean;
set bypassCache(value: boolean);
getAvailableRelay(module: string, method: string): any;
getRelay(pubkey: string): any;
private init;
private setupRelayPromise;
}
//# sourceMappingURL=network.d.ts.map

1
dist/network.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"AAEA,OAAO,sBAAsB,MAAM,kBAAkB,CAAC;AAItD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,OAAO,CAAC,uBAAuB,CAAC,CAAgB;IAChD,OAAO,CAAC,uBAAuB,CAAC,CAAW;gBAC/B,KAAK,MAAmB;IAKpC,OAAO,CAAC,QAAQ,CAA4D;IAE5E,IAAI,OAAO,IAAI,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAEtC;IAED,OAAO,CAAC,QAAQ,CAAoC;IAEpD,IAAI,OAAO,IAAI,sBAAsB,CAEpC;IAED,OAAO,CAAC,MAAM,CAAoB;IAElC,IAAI,KAAK,QAER;IAED,OAAO,CAAC,kBAAkB,CAAQ;IAElC,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED,IAAI,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAElC;IAED,OAAO,CAAC,aAAa,CAAM;IAE3B,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAE7B;IAED,OAAO,CAAC,aAAa,CAAK;IAE1B,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAE7B;IAED,OAAO,CAAC,OAAO,CAAiD;IAEhE,IAAI,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAElC;IAED,OAAO,CAAC,MAAM,CAAC,CAAgB;IAE/B,IAAI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAQzB;IAED,IAAI,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAED,OAAO,CAAC,YAAY,CAAkB;IAEtC,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,IAAI,WAAW,CAAC,KAAK,EAAE,OAAO,EAE7B;IAEM,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAchD,QAAQ,CAAC,MAAM,EAAE,MAAM;IAQ9B,OAAO,CAAC,IAAI;IA+CZ,OAAO,CAAC,iBAAiB;CAK1B"}

124
dist/network.js vendored Normal file
View File

@ -0,0 +1,124 @@
// @ts-ignore
import Hyperswarm from "hyperswarm";
import RpcNetworkQueryFactory from "./query/index.js";
import b4a from "b4a";
import { createHash, maybeGetAsyncProperty } from "./util.js";
export default class RpcNetwork {
_relaysAvailablePromise;
_relaysAvailableResolve;
constructor(swarm = new Hyperswarm()) {
this._swarm = swarm;
this.init();
}
_methods = new Map();
get methods() {
return this._methods;
}
_factory = new RpcNetworkQueryFactory(this);
get factory() {
return this._factory;
}
_swarm;
get swarm() {
return this._swarm;
}
_majorityThreshold = 0.75;
get majorityThreshold() {
return this._majorityThreshold;
}
set majorityThreshold(value) {
this._majorityThreshold = value;
}
_queryTimeout = 30;
get queryTimeout() {
return this._queryTimeout;
}
set queryTimeout(value) {
this._queryTimeout = value;
}
_relayTimeout = 2;
get relayTimeout() {
return this._relayTimeout;
}
set relayTimeout(value) {
this._relayTimeout = value;
}
_relays = new Map();
get relays() {
return this._relays;
}
_ready;
get ready() {
if (!this._ready) {
this._ready = maybeGetAsyncProperty(this._swarm.dht).then((dht) => dht.ready());
}
return this._ready;
}
get readyWithRelays() {
return this.ready.then(() => this._relaysAvailablePromise);
}
_bypassCache = false;
get bypassCache() {
return this._bypassCache;
}
set bypassCache(value) {
this._bypassCache = value;
}
getAvailableRelay(module, method) {
method = `${module}.${method}`;
let relays = this._methods.get(method) ?? new Set();
if (!relays.size) {
throw Error("no available relay");
}
return this._relays.get(Array.from(relays)[Math.floor(Math.random() * relays.size)]);
}
getRelay(pubkey) {
if (this._relays.has(pubkey)) {
return this._relays.get(pubkey);
}
return undefined;
}
init() {
this._swarm.join(createHash("lumeweb"));
this.setupRelayPromise();
this._swarm.on("connection", async (relay) => {
const pubkey = b4a
.from(await maybeGetAsyncProperty(relay.remotePublicKey))
.toString("hex");
relay.once("close", () => {
this._methods.forEach((item) => {
if (item.has(pubkey)) {
item.delete(pubkey);
}
});
this.relays.delete(pubkey);
if (!this._relays.size) {
this.setupRelayPromise();
}
});
const query = this._factory.simple({
relay,
query: { module: "core", method: "get_methods", data: null },
});
const resp = await query.result;
if (resp.error) {
relay.end();
return;
}
if (resp.data) {
this._relays.set(pubkey, relay);
resp.data.forEach((item) => {
const methods = this._methods.get(item) ?? new Set();
methods.add(pubkey);
this._methods.set(item, methods);
});
this._relaysAvailableResolve?.();
}
});
}
setupRelayPromise() {
this._relaysAvailablePromise = new Promise((resolve) => {
this._relaysAvailableResolve = resolve;
});
}
}

38
dist/query/base.d.ts vendored Normal file
View File

@ -0,0 +1,38 @@
/// <reference types="node" />
import RpcNetwork from "../network.js";
import { RpcQueryOptions } from "../types.js";
import type {
ClientRPCRequest,
RPCRequest,
RPCResponse,
} from "@lumeweb/interface-relay";
export default abstract class RpcQueryBase {
protected _network: RpcNetwork;
protected _query: RPCRequest;
protected _options: RpcQueryOptions;
protected _promise?: Promise<any>;
protected _timeoutTimer?: any;
protected _timeout: boolean;
protected _completed: boolean;
protected _response?: RPCResponse;
protected _error?: string;
protected _promiseResolve?: (data: any) => void;
constructor({
network,
query,
options,
}: {
network: RpcNetwork;
query: ClientRPCRequest | RPCRequest;
options: RpcQueryOptions;
});
get result(): Promise<RPCResponse>;
protected handeTimeout(): void;
protected resolve(data?: RPCResponse, timeout?: boolean): void;
run(): this;
private _doRun;
protected setupRelayTimeout(reject: Function): NodeJS.Timeout;
protected abstract _run(): void;
protected queryRpc(rpc: any, request: RPCRequest): Promise<unknown>;
}
//# sourceMappingURL=base.d.ts.map

1
dist/query/base.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/query/base.ts"],"names":[],"mappings":";AACA,OAAO,UAAU,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EACV,gBAAgB,EAChB,UAAU,EACV,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,MAAM,CAAC,OAAO,CAAC,QAAQ,OAAO,YAAY;IACxC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC/B,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC;IAC7B,SAAS,CAAC,QAAQ,EAAE,eAAe,CAAC;IAEpC,SAAS,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,SAAS,CAAC,aAAa,CAAC,EAAE,GAAG,CAAC;IAC9B,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAS;IACpC,SAAS,CAAC,UAAU,EAAE,OAAO,CAAS;IACtC,SAAS,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC;IAClC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;gBAEpC,EACV,OAAO,EACP,KAAK,EACL,OAAY,GACb,EAAE;QACD,OAAO,EAAE,UAAU,CAAC;QACpB,KAAK,EAAE,gBAAgB,GAAG,UAAU,CAAC;QACrC,OAAO,EAAE,eAAe,CAAC;KAC1B;IAMD,IAAI,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC,CAEjC;IAED,SAAS,CAAC,YAAY;IAItB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,OAAO,GAAE,OAAe,GAAG,IAAI;IAc9D,GAAG,IAAI,IAAI;YAmBJ,MAAM;IASpB,SAAS,CAAC,iBAAiB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC,OAAO;IAO7D,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,IAAI;cAEf,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU;CAyBvD"}

84
dist/query/base.js vendored Normal file
View File

@ -0,0 +1,84 @@
import { clearTimeout, setTimeout } from "timers";
export default class RpcQueryBase {
_network;
_query;
_options;
_promise;
_timeoutTimer;
_timeout = false;
_completed = false;
_response;
_error;
_promiseResolve;
constructor({ network, query, options = {}, }) {
this._network = network;
this._query = query;
this._options = options;
}
get result() {
return this._promise;
}
handeTimeout() {
this.resolve(undefined, true);
}
resolve(data, timeout = false) {
clearTimeout(this._timeoutTimer);
this._timeout = timeout;
this._completed = true;
if (timeout) {
data = {
error: "timeout",
};
}
this._promiseResolve?.(data);
}
run() {
this._promise =
this._promise ??
new Promise((resolve) => {
this._promiseResolve = resolve;
});
this._timeoutTimer =
this._timeoutTimer ??
setTimeout(this.handeTimeout.bind(this), (this._options?.queryTimeout || this._network.queryTimeout) * 1000);
this._doRun();
return this;
}
async _doRun() {
try {
await this._network.ready;
await this._run();
}
catch (e) {
this._promiseResolve?.({ error: e?.message || e?.error });
}
}
setupRelayTimeout(reject) {
return setTimeout(() => {
this._error = "timeout";
reject("timeout");
}, (this._options.relayTimeout || this._network.relayTimeout) * 1000);
}
async queryRpc(rpc, request) {
let timer;
return new Promise((resolve, reject) => {
rpc
// @ts-ignore
.request(`${request.module}.${request.method}`, request.data)
.then((resp) => {
if (resp.error) {
throw new Error(resp.error);
}
clearTimeout(timer);
this._response = resp;
resolve(null);
})
.catch((e) => {
this._error = e.message;
reject({ error: e.message });
clearTimeout(timer);
});
timer = this.setupRelayTimeout(reject);
});
}
}

21
dist/query/clearCache.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
import RpcNetwork from "../network.js";
import { RPCRequest } from "@lumeweb/interface-relay";
import { RpcQueryOptions } from "../types.js";
import SimpleRpcQuery from "./simple.js";
export default class ClearCacheRpcQuery extends SimpleRpcQuery {
protected _relays: string[];
constructor({
network,
relays,
query,
options,
}: {
network: RpcNetwork;
relays: string[];
query: RPCRequest;
options: RpcQueryOptions;
});
protected _run(): Promise<void>;
protected queryRelay(): Promise<any>;
}
//# sourceMappingURL=clearCache.d.ts.map

1
dist/query/clearCache.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"clearCache.d.ts","sourceRoot":"","sources":["../../src/query/clearCache.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,eAAe,CAAC;AACvC,OAAO,EAAuB,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,cAAc,MAAM,aAAa,CAAC;AAEzC,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,cAAc;IAC5D,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;gBAEhB,EACV,OAAO,EACP,MAAM,EACN,KAAK,EACL,OAAO,GACR,EAAE;QACD,OAAO,EAAE,UAAU,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,KAAK,EAAE,UAAU,CAAC;QAClB,OAAO,EAAE,eAAe,CAAC;KAC1B;cAKe,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;cAOrB,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC;CAiB3C"}

29
dist/query/clearCache.js vendored Normal file
View File

@ -0,0 +1,29 @@
import { hashQuery } from "../util.js";
import SimpleRpcQuery from "./simple.js";
export default class ClearCacheRpcQuery extends SimpleRpcQuery {
_relays;
constructor({ network, relays, query, options, }) {
super({ network, relay: "", query, options });
this._relays = relays;
}
async _run() {
// @ts-ignore
this._relay = getActiveRelay().stream.remotePublicKey;
await this.queryRelay();
await this.checkResponses();
}
async queryRelay() {
return this.queryRpc(this._network.getAvailableRelay("rpc", "broadcast_request"), {
module: "rpc",
method: "broadcast_request",
data: {
request: {
module: "rpc",
method: "clear_cached_item",
data: hashQuery(this._query),
},
relays: this._relays,
},
});
}
}

31
dist/query/index.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
/// <reference types="node" />
import { ClientRPCRequest, RPCRequest } from "@lumeweb/interface-relay";
import { RpcQueryOptions } from "../types.js";
import SimpleRpcQuery from "./simple.js";
import ClearCacheRpcQuery from "./clearCache.js";
import RpcNetwork from "../network.js";
import RpcQueryBase from "./base.js";
export default class RpcNetworkQueryFactory {
private _network;
constructor(network: RpcNetwork);
simple({
relay,
query,
options,
}: {
relay?: string | Buffer;
query: ClientRPCRequest;
options?: RpcQueryOptions;
}): SimpleRpcQuery;
clearCache({
relays,
query,
options,
}: {
relays: string[];
query: RPCRequest;
options?: RpcQueryOptions;
}): ClearCacheRpcQuery;
}
export { RpcNetwork, RpcQueryBase, SimpleRpcQuery };
//# sourceMappingURL=index.d.ts.map

1
dist/query/index.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,cAAc,MAAM,aAAa,CAAC;AACzC,OAAO,kBAAkB,MAAM,iBAAiB,CAAC;AACjD,OAAO,UAAU,MAAM,eAAe,CAAC;AACvC,OAAO,YAAY,MAAM,WAAW,CAAC;AAErC,MAAM,CAAC,OAAO,OAAO,sBAAsB;IACzC,OAAO,CAAC,QAAQ,CAAa;gBAEjB,OAAO,EAAE,UAAU;IAI/B,MAAM,CAAC,EACL,KAAK,EACL,KAAK,EACL,OAAY,GACb,EAAE;QACD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACxB,KAAK,EAAE,gBAAgB,CAAC;QACxB,OAAO,CAAC,EAAE,eAAe,CAAC;KAC3B,GAAG,cAAc;IAYlB,UAAU,CAAC,EACT,MAAM,EACN,KAAK,EACL,OAAY,GACb,EAAE;QACD,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,KAAK,EAAE,UAAU,CAAC;QAClB,OAAO,CAAC,EAAE,eAAe,CAAC;KAC3B,GAAG,kBAAkB;CAQvB;AAED,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC"}

30
dist/query/index.js vendored Normal file
View File

@ -0,0 +1,30 @@
import SimpleRpcQuery from "./simple.js";
import ClearCacheRpcQuery from "./clearCache.js";
import RpcNetwork from "../network.js";
import RpcQueryBase from "./base.js";
export default class RpcNetworkQueryFactory {
_network;
constructor(network) {
this._network = network;
}
simple({ relay, query, options = {}, }) {
return new SimpleRpcQuery({
network: this._network,
relay,
query: {
...query,
bypassCache: query?.bypassCache || this._network.bypassCache,
},
options,
}).run();
}
clearCache({ relays, query, options = {}, }) {
return new ClearCacheRpcQuery({
network: this._network,
query,
relays,
options,
}).run();
}
}
export { RpcNetwork, RpcQueryBase, SimpleRpcQuery };

24
dist/query/simple.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
/// <reference types="node" />
import RpcNetwork from "../network.js";
import { ClientRPCRequest } from "@lumeweb/interface-relay";
import { RpcQueryOptions } from "../types.js";
import RpcQueryBase from "./base.js";
export default class SimpleRpcQuery extends RpcQueryBase {
protected _relay?: string | any;
protected _query: ClientRPCRequest;
constructor({
network,
relay,
query,
options,
}: {
network: RpcNetwork;
relay?: string | Buffer | any;
query: ClientRPCRequest;
options: RpcQueryOptions;
});
protected _run(): Promise<void>;
protected queryRelay(): Promise<any>;
protected checkResponses(): Promise<void>;
}
//# sourceMappingURL=simple.d.ts.map

1
dist/query/simple.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"simple.d.ts","sourceRoot":"","sources":["../../src/query/simple.ts"],"names":[],"mappings":";AAAA,OAAO,UAAU,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAe,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAW9C,OAAO,YAAY,MAAM,WAAW,CAAC;AAErC,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,YAAY;IACtD,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAChC,UAAkB,MAAM,EAAE,gBAAgB,CAAC;gBAE/B,EACV,OAAO,EACP,KAAK,EACL,KAAK,EACL,OAAO,GACR,EAAE;QACD,OAAO,EAAE,UAAU,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,GAAG,CAAC;QAC9B,KAAK,EAAE,gBAAgB,CAAC;QACxB,OAAO,EAAE,eAAe,CAAC;KAC1B;cASe,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;cAKrB,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC;cAwD1B,cAAc;CAsB/B"}

75
dist/query/simple.js vendored Normal file
View File

@ -0,0 +1,75 @@
import b4a from "b4a";
import { hashQuery, isPromise, maybeGetAsyncProperty, setupStream, validateTimestampedResponse, } from "../util.js";
import { ERR_INVALID_SIGNATURE } from "../error.js";
import RpcQueryBase from "./base.js";
export default class SimpleRpcQuery extends RpcQueryBase {
_relay;
constructor({ network, relay, query, options, }) {
super({ network, query, options });
if (b4a.isBuffer(relay)) {
relay = b4a.from(relay).toString("hex");
}
this._relay = relay;
}
async _run() {
await this.queryRelay();
await this.checkResponses();
}
async queryRelay() {
let socket = this._relay;
if (socket) {
if (typeof socket === "string") {
try {
const relay = this._network.getRelay(socket);
if (this._network.getRelay(socket)) {
socket = relay;
}
}
catch { }
}
if (typeof socket === "string") {
try {
socket = this._network.swarm.connect(b4a.from(this._relay, "hex"));
if (isPromise(socket)) {
socket = await socket;
}
}
catch { }
}
}
if (!socket) {
socket = this._network.getAvailableRelay(this._query.module, this._query.method);
}
this._relay = socket;
await socket.opened;
const rpc = await setupStream(socket);
if (this._query.bypassCache) {
delete this._query.bypassCache;
await this.queryRpc(rpc, {
module: "rpc",
method: "clear_cached_item",
data: hashQuery(this._query),
});
}
if ("bypassCache" in this._query) {
delete this._query.bypassCache;
}
try {
await this.queryRpc(rpc, this._query);
}
catch (e) {
throw e;
}
}
async checkResponses() {
let response = this._response;
if (this._error) {
response = { error: this._error };
}
if (!response.error &&
!validateTimestampedResponse(b4a.from(await maybeGetAsyncProperty(this._relay.remotePublicKey), "hex"), response)) {
response = { error: ERR_INVALID_SIGNATURE };
}
this.resolve(response);
}
}

5
dist/types.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
export interface RpcQueryOptions {
queryTimeout?: number;
relayTimeout?: number;
}
//# sourceMappingURL=types.d.ts.map

1
dist/types.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}

1
dist/types.js vendored Normal file
View File

@ -0,0 +1 @@
export {};

19
dist/util.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
/// <reference types="node" />
import type { RPCRequest, RPCResponse } from "@lumeweb/interface-relay";
export declare const RPC_PROTOCOL_SYMBOL: unique symbol;
export declare function isPromise(obj: Promise<any>): boolean;
export declare function flatten(target: any, opts?: any): any[];
export declare function validateResponse(
relay: Buffer,
response: RPCResponse,
timestamped?: boolean
): boolean;
export declare function validateTimestampedResponse(
relay: Buffer,
response: RPCResponse
): boolean;
export declare function hashQuery(query: RPCRequest): string;
export declare function createHash(data: string): Buffer;
export declare function setupStream(stream: any): Promise<any>;
export declare function maybeGetAsyncProperty(object: any): Promise<any>;
//# sourceMappingURL=util.d.ts.map

1
dist/util.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAOxE,eAAO,MAAM,mBAAmB,eAAwB,CAAC;AAEzD,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,WAM1C;AAKD,wBAAgB,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,GAAE,GAAQ,GAAG,GAAG,EAAE,CA0C1D;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,WAAW,EACrB,WAAW,UAAQ,GAClB,OAAO,CAkBT;AAED,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,WAAW,GACpB,OAAO,CAET;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAUnD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/C;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,GAAG,gBAa5C;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,GAAG,gBAUtD"}

102
dist/util.js vendored Normal file
View File

@ -0,0 +1,102 @@
// @ts-ignore
import stringify from "json-stringify-deterministic";
// @ts-ignore
import crypto from "hypercore-crypto";
// @ts-ignore
import sodium from "sodium-universal";
import b4a from "b4a";
import RPC from "@lumeweb/rpc";
export const RPC_PROTOCOL_SYMBOL = Symbol.for("lumeweb");
export function isPromise(obj) {
return (!!obj &&
(typeof obj === "object" || typeof obj === "function") &&
typeof obj.then === "function");
}
/*
Forked from https://github.com/hughsk/flat
*/
export function flatten(target, opts = {}) {
opts = opts || {};
const delimiter = opts.delimiter || ".";
const maxDepth = opts.maxDepth;
const transformKey = opts.transformKey || ((key) => (isNaN(parseInt(key)) ? key : ""));
const output = [];
function step(object, prev, currentDepth) {
currentDepth = currentDepth || 1;
if (!Array.isArray(object)) {
object = Object.keys(object ?? {});
}
object.forEach(function (key) {
const value = object[key];
const isarray = opts.safe && Array.isArray(value);
const type = Object.prototype.toString.call(value);
const isbuffer = b4a.isBuffer(value);
const isobject = type === "[object Object]" || type === "[object Array]";
const newKey = prev
? prev + delimiter + transformKey(key)
: transformKey(key);
if (!isarray &&
!isbuffer &&
isobject &&
Object.keys(value).length &&
(!opts.maxDepth || currentDepth < maxDepth)) {
return step(value, newKey, currentDepth + 1);
}
output.push(`${newKey}=${value}`);
});
}
step(target);
return output;
}
export function validateResponse(relay, response, timestamped = false) {
const field = response.signedField || "data";
// @ts-ignore
let json = response[field];
if (typeof json !== "string") {
json = stringify(json);
}
const updated = response.updated;
if (timestamped && updated) {
json = updated.toString() + json;
}
return !!crypto.verify(b4a.from(json), b4a.from(response.signature, "hex"), relay);
}
export function validateTimestampedResponse(relay, response) {
return validateResponse(relay, response, true);
}
export function hashQuery(query) {
const clonedQuery = {
module: query.module,
method: query.method,
data: query.data,
};
const queryHash = Buffer.allocUnsafe(32);
sodium.crypto_generichash(queryHash, Buffer.from(stringify(clonedQuery)));
return queryHash.toString("hex");
}
export function createHash(data) {
const buffer = b4a.from(data);
let hash = b4a.allocUnsafe(32);
sodium.crypto_generichash(hash, buffer);
return hash;
}
export async function setupStream(stream) {
const existing = stream[RPC_PROTOCOL_SYMBOL];
if (existing) {
await existing.ready;
return existing;
}
const rpc = new RPC(stream);
stream[RPC_PROTOCOL_SYMBOL] = rpc;
await rpc.ready;
return rpc;
}
export async function maybeGetAsyncProperty(object) {
if (typeof object === "function") {
object = object();
}
if (isPromise(object)) {
object = await object;
}
return object;
}

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "@lumeweb/rpc-client",
"type": "module",
"version": "0.1.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "rimraf dist && tsc"
},
"devDependencies": {
"@lumeweb/interface-relay": "git+https://git.lumeweb.com/LumeWeb/interface-relay.git",
"@types/b4a": "^1.6.0",
"@types/express": "^4.17.14",
"@types/node": "^18.0.0",
"node-cache": "^5.1.2",
"prettier": "^2.7.1",
"typescript": "^4.7.4"
},
"dependencies": {
"@hyperswarm/dht": "^6.0.1",
"@lumeweb/rpc": "git+https://git.lumeweb.com/LumeWeb/rpc.git",
"b4a": "^1.6.1",
"hypercore-crypto": "^3.3.1",
"hyperswarm": "^4.3.7",
"json-stringify-deterministic": "^1.0.7",
"libskynet": "^0.0.61",
"msgpackr": "^1.6.1",
"sodium-universal": "^4.0.0"
}
}

1
src/error.ts Normal file
View File

@ -0,0 +1 @@
export const ERR_INVALID_SIGNATURE = "INVALID_SIGNATURE";

4
src/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from "./types.js";
export * from "./query/index.js";
export * from "./network.js";
export * from "./util.js";

169
src/network.ts Normal file
View File

@ -0,0 +1,169 @@
// @ts-ignore
import Hyperswarm from "hyperswarm";
import RpcNetworkQueryFactory from "./query/index.js";
import b4a from "b4a";
import { createHash, maybeGetAsyncProperty } from "./util.js";
export default class RpcNetwork {
private _relaysAvailablePromise?: Promise<void>;
private _relaysAvailableResolve?: Function;
constructor(swarm = new Hyperswarm()) {
this._swarm = swarm;
this.init();
}
private _methods: Map<string, Set<string>> = new Map<string, Set<string>>();
get methods(): Map<string, Set<string>> {
return this._methods;
}
private _factory = new RpcNetworkQueryFactory(this);
get factory(): RpcNetworkQueryFactory {
return this._factory;
}
private _swarm: typeof Hyperswarm;
get swarm() {
return this._swarm;
}
private _majorityThreshold = 0.75;
get majorityThreshold(): number {
return this._majorityThreshold;
}
set majorityThreshold(value: number) {
this._majorityThreshold = value;
}
private _queryTimeout = 30;
get queryTimeout(): number {
return this._queryTimeout;
}
set queryTimeout(value: number) {
this._queryTimeout = value;
}
private _relayTimeout = 2;
get relayTimeout(): number {
return this._relayTimeout;
}
set relayTimeout(value: number) {
this._relayTimeout = value;
}
private _relays: Map<string, any> = new Map<string, string[]>();
get relays(): Map<string, string[]> {
return this._relays;
}
private _ready?: Promise<void>;
get ready(): Promise<void> {
if (!this._ready) {
this._ready = maybeGetAsyncProperty(this._swarm.dht).then((dht: any) =>
dht.ready()
) as Promise<void>;
}
return this._ready as Promise<void>;
}
get readyWithRelays(): Promise<void> {
return this.ready.then(() => this._relaysAvailablePromise);
}
private _bypassCache: boolean = false;
get bypassCache(): boolean {
return this._bypassCache;
}
set bypassCache(value: boolean) {
this._bypassCache = value;
}
public getAvailableRelay(module: string, method: string) {
method = `${module}.${method}`;
let relays = this._methods.get(method) ?? new Set();
if (!relays.size) {
throw Error("no available relay");
}
return this._relays.get(
Array.from(relays)[Math.floor(Math.random() * relays.size)]
);
}
public getRelay(pubkey: string) {
if (this._relays.has(pubkey)) {
return this._relays.get(pubkey);
}
return undefined;
}
private init() {
this._swarm.join(createHash("lumeweb"));
this.setupRelayPromise();
this._swarm.on("connection", async (relay: any) => {
const pubkey = b4a
.from(await maybeGetAsyncProperty(relay.remotePublicKey))
.toString("hex");
relay.once("close", () => {
this._methods.forEach((item) => {
if (item.has(pubkey)) {
item.delete(pubkey);
}
});
this.relays.delete(pubkey);
if (!this._relays.size) {
this.setupRelayPromise();
}
});
const query = this._factory.simple({
relay,
query: { module: "core", method: "get_methods", data: null },
});
const resp = await query.result;
if (resp.error) {
relay.end();
return;
}
if (resp.data) {
this._relays.set(pubkey, relay);
(resp.data as string[]).forEach((item) => {
const methods: Set<string> =
this._methods.get(item) ?? new Set<string>();
methods.add(pubkey);
this._methods.set(item, methods);
});
this._relaysAvailableResolve?.();
}
});
}
private setupRelayPromise() {
this._relaysAvailablePromise = new Promise<void>((resolve) => {
this._relaysAvailableResolve = resolve;
});
}
}

121
src/query/base.ts Normal file
View File

@ -0,0 +1,121 @@
import { clearTimeout, setTimeout } from "timers";
import RpcNetwork from "../network.js";
import { RpcQueryOptions } from "../types.js";
import type {
ClientRPCRequest,
RPCRequest,
RPCResponse,
} from "@lumeweb/interface-relay";
export default abstract class RpcQueryBase {
protected _network: RpcNetwork;
protected _query: RPCRequest;
protected _options: RpcQueryOptions;
protected _promise?: Promise<any>;
protected _timeoutTimer?: any;
protected _timeout: boolean = false;
protected _completed: boolean = false;
protected _response?: RPCResponse;
protected _error?: string;
protected _promiseResolve?: (data: any) => void;
constructor({
network,
query,
options = {},
}: {
network: RpcNetwork;
query: ClientRPCRequest | RPCRequest;
options: RpcQueryOptions;
}) {
this._network = network;
this._query = query;
this._options = options;
}
get result(): Promise<RPCResponse> {
return this._promise as Promise<RPCResponse>;
}
protected handeTimeout() {
this.resolve(undefined, true);
}
protected resolve(data?: RPCResponse, timeout: boolean = false): void {
clearTimeout(this._timeoutTimer);
this._timeout = timeout;
this._completed = true;
if (timeout) {
data = {
error: "timeout",
};
}
this._promiseResolve?.(data);
}
public run(): this {
this._promise =
this._promise ??
new Promise<any>((resolve) => {
this._promiseResolve = resolve;
});
this._timeoutTimer =
this._timeoutTimer ??
setTimeout(
this.handeTimeout.bind(this),
(this._options?.queryTimeout || this._network.queryTimeout) * 1000
);
this._doRun();
return this;
}
private async _doRun() {
try {
await this._network.ready;
await this._run();
} catch (e: any) {
this._promiseResolve?.({ error: e?.message || e?.error });
}
}
protected setupRelayTimeout(reject: Function): NodeJS.Timeout {
return setTimeout(() => {
this._error = "timeout";
reject("timeout");
}, (this._options.relayTimeout || this._network.relayTimeout) * 1000) as NodeJS.Timeout;
}
protected abstract _run(): void;
protected async queryRpc(rpc: any, request: RPCRequest) {
let timer: NodeJS.Timeout;
return new Promise((resolve, reject) => {
rpc
// @ts-ignore
.request(`${request.module}.${request.method}`, request.data)
.then((resp: any) => {
if (resp.error) {
throw new Error(resp.error);
}
clearTimeout(timer as any);
this._response = resp;
resolve(null);
})
.catch((e: Error) => {
this._error = e.message;
reject({ error: e.message });
clearTimeout(timer as any);
});
timer = this.setupRelayTimeout(reject);
});
}
}

49
src/query/clearCache.ts Normal file
View File

@ -0,0 +1,49 @@
import RpcNetwork from "../network.js";
import { RPCBroadcastRequest, RPCRequest } from "@lumeweb/interface-relay";
import { RpcQueryOptions } from "../types.js";
import { hashQuery } from "../util.js";
import SimpleRpcQuery from "./simple.js";
export default class ClearCacheRpcQuery extends SimpleRpcQuery {
protected _relays: string[];
constructor({
network,
relays,
query,
options,
}: {
network: RpcNetwork;
relays: string[];
query: RPCRequest;
options: RpcQueryOptions;
}) {
super({ network, relay: "", query, options });
this._relays = relays;
}
protected async _run(): Promise<void> {
// @ts-ignore
this._relay = getActiveRelay().stream.remotePublicKey;
await this.queryRelay();
await this.checkResponses();
}
protected async queryRelay(): Promise<any> {
return this.queryRpc(
this._network.getAvailableRelay("rpc", "broadcast_request"),
{
module: "rpc",
method: "broadcast_request",
data: {
request: {
module: "rpc",
method: "clear_cached_item",
data: hashQuery(this._query),
},
relays: this._relays,
} as RPCBroadcastRequest,
}
);
}
}

53
src/query/index.ts Normal file
View File

@ -0,0 +1,53 @@
import { ClientRPCRequest, RPCRequest } from "@lumeweb/interface-relay";
import { RpcQueryOptions } from "../types.js";
import SimpleRpcQuery from "./simple.js";
import ClearCacheRpcQuery from "./clearCache.js";
import RpcNetwork from "../network.js";
import RpcQueryBase from "./base.js";
export default class RpcNetworkQueryFactory {
private _network: RpcNetwork;
constructor(network: RpcNetwork) {
this._network = network;
}
simple({
relay,
query,
options = {},
}: {
relay?: string | Buffer;
query: ClientRPCRequest;
options?: RpcQueryOptions;
}): SimpleRpcQuery {
return new SimpleRpcQuery({
network: this._network,
relay,
query: {
...query,
bypassCache: query?.bypassCache || this._network.bypassCache,
},
options,
}).run();
}
clearCache({
relays,
query,
options = {},
}: {
relays: string[];
query: RPCRequest;
options?: RpcQueryOptions;
}): ClearCacheRpcQuery {
return new ClearCacheRpcQuery({
network: this._network,
query,
relays,
options,
}).run();
}
}
export { RpcNetwork, RpcQueryBase, SimpleRpcQuery };

122
src/query/simple.ts Normal file
View File

@ -0,0 +1,122 @@
import RpcNetwork from "../network.js";
import { ClientRPCRequest, RPCResponse } from "@lumeweb/interface-relay";
import { RpcQueryOptions } from "../types.js";
import b4a from "b4a";
import {
hashQuery,
isPromise,
maybeGetAsyncProperty,
setupStream,
validateTimestampedResponse,
} from "../util.js";
import RPC from "@lumeweb/rpc";
import { ERR_INVALID_SIGNATURE } from "../error.js";
import RpcQueryBase from "./base.js";
export default class SimpleRpcQuery extends RpcQueryBase {
protected _relay?: string | any;
protected declare _query: ClientRPCRequest;
constructor({
network,
relay,
query,
options,
}: {
network: RpcNetwork;
relay?: string | Buffer | any;
query: ClientRPCRequest;
options: RpcQueryOptions;
}) {
super({ network, query, options });
if (b4a.isBuffer(relay)) {
relay = b4a.from(relay).toString("hex");
}
this._relay = relay;
}
protected async _run(): Promise<void> {
await this.queryRelay();
await this.checkResponses();
}
protected async queryRelay(): Promise<any> {
let socket = this._relay;
if (socket) {
if (typeof socket === "string") {
try {
const relay = this._network.getRelay(socket);
if (this._network.getRelay(socket)) {
socket = relay;
}
} catch {}
}
if (typeof socket === "string") {
try {
socket = this._network.swarm.connect(b4a.from(this._relay, "hex"));
if (isPromise(socket)) {
socket = await socket;
}
} catch {}
}
}
if (!socket) {
socket = this._network.getAvailableRelay(
this._query.module,
this._query.method
);
}
this._relay = socket;
await socket.opened;
const rpc = await setupStream(socket);
if (this._query.bypassCache) {
delete this._query.bypassCache;
await this.queryRpc(rpc, {
module: "rpc",
method: "clear_cached_item",
data: hashQuery(this._query),
});
}
if ("bypassCache" in this._query) {
delete this._query.bypassCache;
}
try {
await this.queryRpc(rpc, this._query);
} catch (e: any) {
throw e;
}
}
protected async checkResponses() {
let response: RPCResponse = this._response as RPCResponse;
if (this._error) {
response = { error: this._error };
}
if (
!response.error &&
!validateTimestampedResponse(
b4a.from(
await maybeGetAsyncProperty(this._relay.remotePublicKey),
"hex"
) as Buffer,
response
)
) {
response = { error: ERR_INVALID_SIGNATURE };
}
this.resolve(response);
}
}

4
src/types.ts Normal file
View File

@ -0,0 +1,4 @@
export interface RpcQueryOptions {
queryTimeout?: number;
relayTimeout?: number;
}

143
src/util.ts Normal file
View File

@ -0,0 +1,143 @@
// @ts-ignore
import stringify from "json-stringify-deterministic";
import type { RPCRequest, RPCResponse } from "@lumeweb/interface-relay";
// @ts-ignore
import crypto from "hypercore-crypto";
// @ts-ignore
import sodium from "sodium-universal";
import b4a from "b4a";
import RPC from "@lumeweb/rpc";
export const RPC_PROTOCOL_SYMBOL = Symbol.for("lumeweb");
export function isPromise(obj: Promise<any>) {
return (
!!obj &&
(typeof obj === "object" || typeof obj === "function") &&
typeof obj.then === "function"
);
}
/*
Forked from https://github.com/hughsk/flat
*/
export function flatten(target: any, opts: any = {}): any[] {
opts = opts || {};
const delimiter = opts.delimiter || ".";
const maxDepth = opts.maxDepth;
const transformKey =
opts.transformKey || ((key: any) => (isNaN(parseInt(key)) ? key : ""));
const output: any[] = [];
function step(object: any, prev?: any, currentDepth?: any) {
currentDepth = currentDepth || 1;
if (!Array.isArray(object)) {
object = Object.keys(object ?? {});
}
object.forEach(function (key: any) {
const value = object[key];
const isarray = opts.safe && Array.isArray(value);
const type = Object.prototype.toString.call(value);
const isbuffer = b4a.isBuffer(value);
const isobject = type === "[object Object]" || type === "[object Array]";
const newKey = prev
? prev + delimiter + transformKey(key)
: transformKey(key);
if (
!isarray &&
!isbuffer &&
isobject &&
Object.keys(value).length &&
(!opts.maxDepth || currentDepth < maxDepth)
) {
return step(value, newKey, currentDepth + 1);
}
output.push(`${newKey}=${value}`);
});
}
step(target);
return output;
}
export function validateResponse(
relay: Buffer,
response: RPCResponse,
timestamped = false
): boolean {
const field = response.signedField || "data";
// @ts-ignore
let json = response[field];
if (typeof json !== "string") {
json = stringify(json);
}
const updated = response.updated as number;
if (timestamped && updated) {
json = updated.toString() + json;
}
return !!crypto.verify(
b4a.from(json),
b4a.from(response.signature as string, "hex"),
relay
);
}
export function validateTimestampedResponse(
relay: Buffer,
response: RPCResponse
): boolean {
return validateResponse(relay, response, true);
}
export function hashQuery(query: RPCRequest): string {
const clonedQuery: RPCRequest = {
module: query.module,
method: query.method,
data: query.data,
};
const queryHash = Buffer.allocUnsafe(32);
sodium.crypto_generichash(queryHash, Buffer.from(stringify(clonedQuery)));
return queryHash.toString("hex");
}
export function createHash(data: string): Buffer {
const buffer = b4a.from(data);
let hash = b4a.allocUnsafe(32) as Buffer;
sodium.crypto_generichash(hash, buffer);
return hash;
}
export async function setupStream(stream: any) {
const existing = stream[RPC_PROTOCOL_SYMBOL];
if (existing) {
await existing.ready;
return existing;
}
const rpc = new RPC(stream);
stream[RPC_PROTOCOL_SYMBOL] = rpc;
await rpc.ready;
return rpc;
}
export async function maybeGetAsyncProperty(object: any) {
if (typeof object === "function") {
object = object();
}
if (isPromise(object)) {
object = await object;
}
return object;
}

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"declaration": true,
"strict": true,
"module": "esnext",
"target": "esnext",
"esModuleInterop": true,
"sourceMap": false,
"rootDir": "src",
"outDir": "dist",
"typeRoots": [
"node_modules/@types",
],
"moduleResolution": "node",
"declarationMap": true,
"declarationDir": "dist",
"emitDeclarationOnly": false,
"allowJs": true
},
"include": [
"src"
]
}