@peerbit/rpc 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { field, fixedArray, option, variant } from "@dao-xyz/borsh";
11
+ import { MaybeEncrypted, X25519PublicKey } from "@peerbit/crypto";
12
+ export let RPCMessage = class RPCMessage {
13
+ };
14
+ RPCMessage = __decorate([
15
+ variant(0)
16
+ ], RPCMessage);
17
+ export let RequestV0 = class RequestV0 extends RPCMessage {
18
+ respondTo;
19
+ request;
20
+ constructor(properties) {
21
+ super();
22
+ this.respondTo = properties.respondTo;
23
+ this.request = properties.request;
24
+ }
25
+ };
26
+ __decorate([
27
+ field({ type: option(X25519PublicKey) }),
28
+ __metadata("design:type", X25519PublicKey)
29
+ ], RequestV0.prototype, "respondTo", void 0);
30
+ __decorate([
31
+ field({ type: MaybeEncrypted }),
32
+ __metadata("design:type", MaybeEncrypted)
33
+ ], RequestV0.prototype, "request", void 0);
34
+ RequestV0 = __decorate([
35
+ variant(0),
36
+ __metadata("design:paramtypes", [Object])
37
+ ], RequestV0);
38
+ export let ResponseV0 = class ResponseV0 extends RPCMessage {
39
+ requestId;
40
+ response;
41
+ constructor(properties) {
42
+ super();
43
+ this.response = properties.response;
44
+ this.requestId = properties.requestId;
45
+ }
46
+ };
47
+ __decorate([
48
+ field({ type: fixedArray("u8", 32) }),
49
+ __metadata("design:type", Uint8Array)
50
+ ], ResponseV0.prototype, "requestId", void 0);
51
+ __decorate([
52
+ field({ type: MaybeEncrypted }),
53
+ __metadata("design:type", MaybeEncrypted)
54
+ ], ResponseV0.prototype, "response", void 0);
55
+ ResponseV0 = __decorate([
56
+ variant(1),
57
+ __metadata("design:paramtypes", [Object])
58
+ ], ResponseV0);
59
+ //# sourceMappingURL=encoding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoding.js","sourceRoot":"","sources":["../../src/encoding.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAG3D,WAAe,UAAU,GAAzB,MAAe,UAAU;CAAG,CAAA;AAAb,UAAU;IAD/B,OAAO,CAAC,CAAC,CAAC;GACW,UAAU,CAAG;AAG5B,WAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,UAAU;IAExC,SAAS,CAAmB;IAG5B,OAAO,CAAsB;IAE7B,YAAY,UAGX;QACA,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;QACtC,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC;IACnC,CAAC;CACD,CAAA;AAbA;IADC,KAAK,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC;8BAC7B,eAAe;4CAAC;AAG5B;IADC,KAAK,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;8BACvB,cAAc;0CAAM;AALjB,SAAS;IADrB,OAAO,CAAC,CAAC,CAAC;;GACE,SAAS,CAerB;AAGM,WAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,UAAU;IAEzC,SAAS,CAAa;IAGtB,QAAQ,CAAsB;IAE9B,YAAY,UAGX;QACA,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC;QACpC,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;IACvC,CAAC;CACD,CAAA;AAbA;IADC,KAAK,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;8BAC3B,UAAU;6CAAC;AAGtB;IADC,KAAK,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;8BACtB,cAAc;4CAAM;AALlB,UAAU;IADtB,OAAO,CAAC,CAAC,CAAC;;GACE,UAAU,CAetB"}
@@ -0,0 +1,4 @@
1
+ export * from "./encoding.js";
2
+ export * from "./io.js";
3
+ export * from "./controller.js";
4
+ export * from "./utils.js";
@@ -0,0 +1,5 @@
1
+ export * from "./encoding.js";
2
+ export * from "./io.js";
3
+ export * from "./controller.js";
4
+ export * from "./utils.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { X25519PublicKey, Ed25519PublicKey, PublicSignKey, X25519Keypair } from "@peerbit/crypto";
2
+ export declare const logger: import("pino").Logger<import("pino").LoggerOptions | import("pino").DestinationStream>;
3
+ export type RPCOptions<R> = {
4
+ amount?: number;
5
+ timeout?: number;
6
+ isTrusted?: (publicKey: PublicSignKey) => Promise<boolean>;
7
+ strict?: boolean;
8
+ onResponse?: (response: R, from?: PublicSignKey) => void;
9
+ stopper?: (stopper: () => void) => void;
10
+ } & PublishOptions;
11
+ export type PublishOptions = {
12
+ encryption?: {
13
+ key: X25519Keypair;
14
+ responders?: (X25519PublicKey | Ed25519PublicKey)[];
15
+ };
16
+ to?: PublicSignKey[] | string[];
17
+ strict?: boolean;
18
+ };
19
+ export type RPCResponse<R> = {
20
+ response: R;
21
+ from?: PublicSignKey;
22
+ };
package/lib/esm/io.js ADDED
@@ -0,0 +1,3 @@
1
+ import { logger as loggerFn } from "@peerbit/logger";
2
+ export const logger = loggerFn({ module: "rpc" });
3
+ //# sourceMappingURL=io.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.js","sourceRoot":"","sources":["../../src/io.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,IAAI,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,CAAC,MAAM,MAAM,GAAG,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,6 @@
1
+ import { RPC } from "./controller";
2
+ import { RPCOptions, RPCResponse } from "./io";
3
+ export declare class MissingResponsesError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export declare const queryAll: <Q, R>(rpc: RPC<Q, R>, groups: string[][], request: Q, responseHandler: (response: RPCResponse<R>[]) => Promise<void> | void, options?: RPCOptions<R> | undefined) => Promise<void>;
@@ -0,0 +1,57 @@
1
+ export class MissingResponsesError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ }
5
+ }
6
+ export const queryAll = (rpc, groups, request, responseHandler, options) => {
7
+ // In each shard/group only query a subset
8
+ groups = [...groups].filter((x) => !x.find((y) => y === rpc.node.identity.publicKey.hashcode()));
9
+ let rng = Math.round(Math.random() * groups.length);
10
+ const startRng = rng;
11
+ const fn = async () => {
12
+ let missingReponses = false;
13
+ while (groups.length > 0) {
14
+ const peersToQuery = new Array(groups.length);
15
+ let counter = 0;
16
+ const peerToGroupIndex = new Map();
17
+ for (let i = 0; i < groups.length; i++) {
18
+ const group = groups[i];
19
+ peersToQuery[counter] = group[rng % group.length];
20
+ peerToGroupIndex.set(peersToQuery[counter], i);
21
+ counter++;
22
+ }
23
+ if (peersToQuery.length > 0) {
24
+ const results = await rpc.request(request, {
25
+ ...options,
26
+ to: peersToQuery,
27
+ });
28
+ for (const result of results) {
29
+ if (!result.from) {
30
+ throw new Error("Unexpected, missing from");
31
+ }
32
+ peerToGroupIndex.delete(result.from.hashcode());
33
+ }
34
+ await responseHandler(results);
35
+ const indicesLeft = new Set([...peerToGroupIndex.values()]);
36
+ rng += 1;
37
+ groups = groups.filter((v, ix) => {
38
+ if (indicesLeft.has(ix)) {
39
+ const peerIndex = rng % v.length;
40
+ if (rng === startRng || peerIndex === startRng % v.length) {
41
+ // TODO Last condition needed?
42
+ missingReponses = true;
43
+ return false;
44
+ }
45
+ return true;
46
+ }
47
+ return false;
48
+ });
49
+ }
50
+ }
51
+ if (missingReponses) {
52
+ throw new MissingResponsesError("Did not recieve responses from all shards");
53
+ }
54
+ };
55
+ return fn();
56
+ };
57
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@peerbit/rpc",
3
+ "version": "1.0.2",
4
+ "description": "RPC calls for peers",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "module": "lib/esm/index.js",
8
+ "types": "lib/esm/index.d.ts",
9
+ "exports": {
10
+ "import": "./lib/esm/index.js",
11
+ "require": "./lib/cjs/index.js"
12
+ },
13
+ "files": [
14
+ "lib",
15
+ "src",
16
+ "!src/**/__tests__",
17
+ "!lib/**/__tests__",
18
+ "!documentation",
19
+ "LICENSE"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "clean": "shx rm -rf lib/*",
26
+ "build": "yarn clean && tsc -p tsconfig.json",
27
+ "postbuild": "echo '{\"type\":\"module\"} ' | node ../../../node_modules/.bin/json > lib/esm/package.json",
28
+ "test": "node ../../../node_modules/.bin/jest test -c ../../../jest.config.ts --runInBand --forceExit",
29
+ "test:unit": "node ../../../node_modules/.bin/jest test -c ../../../jest.config.unit.ts --runInBand --forceExit",
30
+ "test:integration": "node ../node_modules/.bin/jest test -c ../../../jest.config.integration.ts --runInBand --forceExit"
31
+ },
32
+ "author": "dao.xyz",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@dao-xyz/borsh": "^5.1.5",
36
+ "@peerbit/crypto": "1.0.1",
37
+ "@peerbit/logger": "1.0.0",
38
+ "@peerbit/program": "1.0.1",
39
+ "@peerbit/time": "1.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@peerbit/test-utils": "^1.0.2"
43
+ },
44
+ "gitHead": "595db9f1efebf604393eddfff5f678f5d8f16142"
45
+ }
@@ -0,0 +1,413 @@
1
+ import {
2
+ AbstractType,
3
+ BorshError,
4
+ deserialize,
5
+ serialize,
6
+ variant,
7
+ } from "@dao-xyz/borsh";
8
+ import {
9
+ DecryptedThing,
10
+ MaybeEncrypted,
11
+ PublicSignKey,
12
+ toBase64,
13
+ AccessError,
14
+ X25519PublicKey,
15
+ X25519Keypair,
16
+ } from "@peerbit/crypto";
17
+ import { RequestV0, ResponseV0, RPCMessage } from "./encoding.js";
18
+ import { RPCOptions, logger, RPCResponse, PublishOptions } from "./io.js";
19
+ import { AbstractProgram, Address } from "@peerbit/program";
20
+ import {
21
+ PubSubData,
22
+ PublishOptions as PubSubPublishOptions,
23
+ } from "@peerbit/pubsub-interface";
24
+ import { ComposableProgram } from "@peerbit/program";
25
+ import { DataMessage } from "@peerbit/stream-interface";
26
+ import pDefer, { DeferredPromise } from "p-defer";
27
+ import { waitFor } from "@peerbit/time";
28
+
29
+ export type SearchContext = (() => Address) | AbstractProgram;
30
+ export type CanRead = (key?: PublicSignKey) => Promise<boolean> | boolean;
31
+
32
+ export type RPCSetupOptions<Q, R> = {
33
+ topic: string;
34
+ queryType: AbstractType<Q>;
35
+ responseType: AbstractType<R>;
36
+ canRead?: CanRead;
37
+ responseHandler?: ResponseHandler<Q, R>;
38
+ subscriptionData?: Uint8Array;
39
+ };
40
+ export type QueryContext = {
41
+ from?: PublicSignKey;
42
+ address: string;
43
+ };
44
+ export type ResponseHandler<Q, R> = (
45
+ query: Q,
46
+ context: QueryContext
47
+ ) => Promise<R | undefined> | R | undefined;
48
+
49
+ const createValueResolver = <T>(
50
+ type: AbstractType<T> | Uint8Array
51
+ ): ((decryptedThings: DecryptedThing<T>) => T) => {
52
+ if ((type as any) === Uint8Array) {
53
+ return (decrypted) => decrypted._data as T;
54
+ } else {
55
+ return (decrypted) => decrypted.getValue(type as AbstractType<T>);
56
+ }
57
+ };
58
+
59
+ @variant("rpc")
60
+ export class RPC<Q, R> extends ComposableProgram<RPCSetupOptions<Q, R>> {
61
+ canRead: CanRead;
62
+
63
+ private _subscribed = false;
64
+ private _responseHandler?: ResponseHandler<Q, (R | undefined) | R>;
65
+ private _responseResolver: Map<
66
+ string,
67
+ (properties: { response: ResponseV0; message: DataMessage }) => any
68
+ >;
69
+ private _requestType: AbstractType<Q> | Uint8ArrayConstructor;
70
+ private _responseType: AbstractType<R>;
71
+ private _rpcTopic: string | undefined;
72
+ private _onMessageBinded: ((arg: any) => any) | undefined = undefined;
73
+ private _subscriptionMetaData: Uint8Array | undefined;
74
+
75
+ private _keypair: X25519Keypair;
76
+
77
+ private _getResponseValueFn: (decrypted: DecryptedThing<R>) => R;
78
+ private _getRequestValueFn: (decrypted: DecryptedThing<Q>) => Q;
79
+
80
+ async open(args: RPCSetupOptions<Q, R>): Promise<void> {
81
+ this._rpcTopic = args.topic ?? this._rpcTopic;
82
+ this._responseHandler = args.responseHandler;
83
+ this._requestType = args.queryType;
84
+ this._responseType = args.responseType;
85
+ this._responseResolver = new Map();
86
+ this._subscriptionMetaData = args.subscriptionData;
87
+ this.canRead = args.canRead || (() => Promise.resolve(true));
88
+
89
+ this._getResponseValueFn = createValueResolver(this._responseType);
90
+ this._getRequestValueFn = createValueResolver(this._requestType);
91
+
92
+ this._keypair = await X25519Keypair.create();
93
+ await this._subscribe();
94
+ }
95
+
96
+ public async close(from?: AbstractProgram): Promise<boolean> {
97
+ if (this._subscribed) {
98
+ await this.node.services.pubsub.unsubscribe(this.rpcTopic);
99
+ await this.node.services.pubsub.removeEventListener(
100
+ "data",
101
+ this._onMessage
102
+ );
103
+ this._subscribed = false;
104
+ }
105
+
106
+ return super.close(from);
107
+ }
108
+
109
+ private _subscribing: Promise<void>;
110
+ private async _subscribe(): Promise<void> {
111
+ await this._subscribing;
112
+ if (this._subscribed) {
113
+ return;
114
+ }
115
+ this._subscribed = true;
116
+ this._onMessageBinded = this._onMessageBinded || this._onMessage.bind(this);
117
+ this._subscribing = this.node.services.pubsub
118
+ .subscribe(this.rpcTopic, { data: this._subscriptionMetaData })
119
+ .then(() => {
120
+ this.node.services.pubsub.addEventListener(
121
+ "data",
122
+ this._onMessageBinded!
123
+ );
124
+ });
125
+
126
+ await this._subscribing;
127
+ await this.node.services.pubsub.requestSubscribers(this.rpcTopic);
128
+
129
+ logger.debug("subscribing to query topic (responses): " + this.rpcTopic);
130
+ }
131
+
132
+ async _onMessage(
133
+ evt: CustomEvent<{ data: PubSubData; message: DataMessage }>
134
+ ): Promise<void> {
135
+ const { data, message } = evt.detail;
136
+
137
+ if (data?.topics.find((x) => x === this.rpcTopic) != null) {
138
+ try {
139
+ const rpcMessage = deserialize(data.data, RPCMessage);
140
+ if (rpcMessage instanceof RequestV0) {
141
+ if (this._responseHandler) {
142
+ const maybeEncrypted = rpcMessage.request;
143
+ const decrypted = await maybeEncrypted.decrypt(this.node.keychain);
144
+
145
+ if (!(await this.canRead(message.sender))) {
146
+ throw new AccessError();
147
+ }
148
+
149
+ const response = await this._responseHandler(
150
+ this._getRequestValueFn(decrypted),
151
+ {
152
+ address: this.rpcTopic,
153
+ from: message.sender,
154
+ }
155
+ );
156
+
157
+ if (response && rpcMessage.respondTo) {
158
+ // send query and wait for replies in a generator like behaviour
159
+ const serializedResponse = serialize(response);
160
+
161
+ // we use the peerId/libp2p identity for signatures, since we want to be able to send a message
162
+ // with pubsub with a certain reciever. If we use (this.identity) we are going to use an identity
163
+ // that is now known in the .pubsub network, hence the message might not be delivired if we
164
+ // send with { to: [RECIEVER] } param
165
+
166
+ const decryptedMessage = new DecryptedThing<Uint8Array>({
167
+ data: serializedResponse,
168
+ });
169
+ let maybeEncryptedMessage: MaybeEncrypted<Uint8Array> =
170
+ decryptedMessage;
171
+
172
+ maybeEncryptedMessage = await decryptedMessage.encrypt(
173
+ this._keypair,
174
+ rpcMessage.respondTo
175
+ );
176
+
177
+ await this.node.services.pubsub.publish(
178
+ serialize(
179
+ new ResponseV0({
180
+ response: maybeEncryptedMessage,
181
+ requestId: message.id,
182
+ })
183
+ ),
184
+ {
185
+ topics: [this.rpcTopic],
186
+ to: [message.sender],
187
+ strict: true,
188
+ }
189
+ );
190
+ }
191
+ }
192
+ } else if (rpcMessage instanceof ResponseV0) {
193
+ const id = toBase64(rpcMessage.requestId);
194
+ let handler = this._responseResolver.get(id);
195
+ if (!handler) {
196
+ handler = await waitFor(() => this._responseResolver.get(id));
197
+ }
198
+ handler!({
199
+ message,
200
+ response: rpcMessage,
201
+ });
202
+ }
203
+ } catch (error: any) {
204
+ if (error instanceof AccessError) {
205
+ logger.debug("Got message I could not decrypt");
206
+ return;
207
+ }
208
+
209
+ if (error instanceof BorshError) {
210
+ logger.error("Got message for a different namespace");
211
+ return;
212
+ }
213
+ logger.error(
214
+ "Error handling query: " +
215
+ (error?.message ? error?.message?.toString() : error)
216
+ );
217
+ }
218
+ }
219
+ }
220
+
221
+ private async seal(
222
+ request: Q,
223
+ respondTo?: X25519PublicKey,
224
+ options?: PublishOptions
225
+ ) {
226
+ const requestData =
227
+ (this._requestType as any) === Uint8Array
228
+ ? (request as Uint8Array)
229
+ : serialize(request);
230
+
231
+ const decryptedMessage = new DecryptedThing<Uint8Array>({
232
+ data: requestData,
233
+ });
234
+ let maybeEncryptedMessage: MaybeEncrypted<Uint8Array> = decryptedMessage;
235
+
236
+ if (
237
+ options?.encryption?.responders &&
238
+ options?.encryption?.responders.length > 0
239
+ ) {
240
+ maybeEncryptedMessage = await decryptedMessage.encrypt(
241
+ options.encryption.key,
242
+ ...options.encryption.responders
243
+ );
244
+ }
245
+
246
+ const requestMessage = new RequestV0({
247
+ request: maybeEncryptedMessage,
248
+ respondTo,
249
+ });
250
+
251
+ return requestMessage;
252
+ }
253
+
254
+ private getPublishOptions(options?: PublishOptions): PubSubPublishOptions {
255
+ return options?.to
256
+ ? { to: options.to, strict: true, topics: [this.rpcTopic] }
257
+ : { topics: [this.rpcTopic] };
258
+ }
259
+
260
+ /**
261
+ * Send message and don't expect any response
262
+ * @param message
263
+ * @param options
264
+ */
265
+ public async send(message: Q, options?: PublishOptions): Promise<void> {
266
+ await this.node.services.pubsub.publish(
267
+ serialize(await this.seal(message, undefined, options)),
268
+ this.getPublishOptions(options)
269
+ );
270
+ }
271
+
272
+ private createResponseHandler(
273
+ promise: DeferredPromise<any>,
274
+ keypair: X25519Keypair,
275
+ allResults: RPCResponse<R>[],
276
+ responders: Set<string>,
277
+ expectedResponders?: Set<string>,
278
+ options?: RPCOptions<R>
279
+ ) {
280
+ return async (properties: {
281
+ response: ResponseV0;
282
+ message: DataMessage;
283
+ }) => {
284
+ try {
285
+ const { response, message } = properties;
286
+ const from = message.sender;
287
+
288
+ if (options?.isTrusted && !(await options?.isTrusted(from))) {
289
+ return;
290
+ }
291
+
292
+ const maybeEncrypted = response.response;
293
+ const decrypted = await maybeEncrypted.decrypt(keypair);
294
+ const resultData = this._getResponseValueFn(decrypted);
295
+
296
+ if (expectedResponders) {
297
+ if (from && expectedResponders?.has(from.hashcode())) {
298
+ options?.onResponse && options?.onResponse(resultData, from);
299
+ allResults.push({ response: resultData, from });
300
+ responders.add(from.hashcode());
301
+ if (responders.size === expectedResponders.size) {
302
+ promise.resolve();
303
+ }
304
+ }
305
+ } else {
306
+ options?.onResponse && options?.onResponse(resultData, from);
307
+ allResults.push({ response: resultData, from });
308
+ if (
309
+ options?.amount != null &&
310
+ allResults.length >= (options?.amount as number)
311
+ ) {
312
+ promise.resolve();
313
+ }
314
+ }
315
+ } catch (error) {
316
+ if (error instanceof AccessError) {
317
+ return; // Ignore things we can not open
318
+ }
319
+
320
+ if (error instanceof BorshError && !options?.strict) {
321
+ logger.debug("Namespace error");
322
+ return; // Name space conflict most likely
323
+ }
324
+
325
+ console.error("failed ot deserialize query response", error);
326
+ promise.reject(error);
327
+ }
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Send a request and expect a response
333
+ * @param request
334
+ * @param options
335
+ * @returns
336
+ */
337
+ public async request(
338
+ request: Q,
339
+ options?: RPCOptions<R>
340
+ ): Promise<RPCResponse<R>[]> {
341
+ // We are generatinga new encryption keypair for each send, so we now that when we get the responses, they are encrypted specifcally for me, and for this request
342
+ // this allows us to easily disregard a bunch of message just beacuse they are for a different reciever!
343
+ const keypair = await X25519Keypair.create();
344
+
345
+ // send query and wait for replies in a generator like behaviour
346
+ let timeoutFn: any = undefined;
347
+
348
+ const requestMessage = await this.seal(request, keypair.publicKey, options);
349
+ const requestBytes = serialize(requestMessage);
350
+
351
+ const allResults: RPCResponse<R>[] = [];
352
+
353
+ const deferredPromise = pDefer();
354
+ options?.stopper && options.stopper(deferredPromise.resolve);
355
+ timeoutFn = setTimeout(() => {
356
+ deferredPromise.resolve();
357
+ }, options?.timeout || 10 * 1000);
358
+
359
+ const expectedResponders =
360
+ options?.to && options.to.length > 0
361
+ ? new Set(
362
+ options.to.map((x) => (typeof x === "string" ? x : x.hashcode()))
363
+ )
364
+ : undefined;
365
+
366
+ const responders = new Set<string>();
367
+
368
+ const id = toBase64(
369
+ await this.node.services.pubsub.publish(
370
+ requestBytes,
371
+ this.getPublishOptions(options)
372
+ )
373
+ );
374
+ this._responseResolver.set(
375
+ id,
376
+ this.createResponseHandler(
377
+ deferredPromise,
378
+ keypair,
379
+ allResults,
380
+ responders,
381
+ expectedResponders,
382
+ options
383
+ )
384
+ );
385
+
386
+ try {
387
+ await deferredPromise.promise;
388
+ } catch (error: any) {
389
+ // timeout
390
+ if (error.constructor.name != "TimeoutError") {
391
+ throw new Error(
392
+ "Got unexpected error when query: " + error.constructor.name
393
+ );
394
+ }
395
+ } finally {
396
+ clearTimeout(timeoutFn);
397
+ }
398
+
399
+ this._responseResolver.delete(id);
400
+ return allResults;
401
+ }
402
+
403
+ public get rpcTopic(): string {
404
+ if (!this._rpcTopic) {
405
+ throw new Error("Not initialized");
406
+ }
407
+ return this._rpcTopic;
408
+ }
409
+
410
+ getTopics(): string[] {
411
+ return [this.rpcTopic];
412
+ }
413
+ }
@@ -0,0 +1,41 @@
1
+ import { field, fixedArray, option, variant } from "@dao-xyz/borsh";
2
+ import { MaybeEncrypted, X25519PublicKey } from "@peerbit/crypto";
3
+
4
+ @variant(0)
5
+ export abstract class RPCMessage {}
6
+
7
+ @variant(0)
8
+ export class RequestV0 extends RPCMessage {
9
+ @field({ type: option(X25519PublicKey) })
10
+ respondTo?: X25519PublicKey;
11
+
12
+ @field({ type: MaybeEncrypted })
13
+ request: MaybeEncrypted<any>;
14
+
15
+ constructor(properties: {
16
+ request: MaybeEncrypted<any>;
17
+ respondTo?: X25519PublicKey;
18
+ }) {
19
+ super();
20
+ this.respondTo = properties.respondTo;
21
+ this.request = properties.request;
22
+ }
23
+ }
24
+
25
+ @variant(1)
26
+ export class ResponseV0 extends RPCMessage {
27
+ @field({ type: fixedArray("u8", 32) })
28
+ requestId: Uint8Array;
29
+
30
+ @field({ type: MaybeEncrypted })
31
+ response: MaybeEncrypted<any>;
32
+
33
+ constructor(properties: {
34
+ response: MaybeEncrypted<any>;
35
+ requestId: Uint8Array;
36
+ }) {
37
+ super();
38
+ this.response = properties.response;
39
+ this.requestId = properties.requestId;
40
+ }
41
+ }