@peerbit/shared-log-proxy 0.0.0-e209d2e

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.
package/src/client.ts ADDED
@@ -0,0 +1,639 @@
1
+ import { deserialize, serialize } from "@dao-xyz/borsh";
2
+ import { createProxyFromService } from "@dao-xyz/borsh-rpc";
3
+ import {
4
+ type CanonicalClient,
5
+ createMessagePortTransport,
6
+ } from "@peerbit/canonical-client";
7
+ import type { PublicSignKey } from "@peerbit/crypto";
8
+ import {
9
+ type CountOptions,
10
+ IdKey,
11
+ type IterateOptions,
12
+ toQuery,
13
+ toSort,
14
+ } from "@peerbit/indexer-interface";
15
+ import { Entry, NO_ENCODING } from "@peerbit/log";
16
+ import {
17
+ type FixedReplicationOptions,
18
+ type LogResultsIterator,
19
+ type ReplicationOptions,
20
+ type ReplicationRangeIndexable,
21
+ ReplicationRangeIndexableU32,
22
+ ReplicationRangeIndexableU64,
23
+ } from "@peerbit/shared-log";
24
+ import type { SharedLogLike } from "@peerbit/shared-log";
25
+ import {
26
+ OpenSharedLogRequest,
27
+ SharedLogBytes,
28
+ SharedLogCoverageRequest,
29
+ SharedLogEntriesBatch,
30
+ SharedLogEntriesIteratorService,
31
+ SharedLogEvent,
32
+ SharedLogReplicateBool,
33
+ SharedLogReplicateFactor,
34
+ SharedLogReplicateFixed,
35
+ SharedLogReplicateFixedList,
36
+ SharedLogReplicateRequest,
37
+ SharedLogReplicateValue,
38
+ SharedLogReplicationBatch,
39
+ SharedLogReplicationCountRequest,
40
+ SharedLogReplicationIterateRequest,
41
+ SharedLogReplicationIteratorService,
42
+ SharedLogReplicationRange,
43
+ SharedLogService,
44
+ SharedLogUnreplicateRequest,
45
+ SharedLogWaitForReplicatorRequest,
46
+ SharedLogWaitForReplicatorsRequest,
47
+ } from "./protocol.js";
48
+
49
+ const ensureCustomEvent = () => {
50
+ if (typeof (globalThis as any).CustomEvent === "function") {
51
+ return;
52
+ }
53
+
54
+ class CustomEventPolyfill<T = any> extends Event {
55
+ detail: T;
56
+ constructor(type: string, params?: CustomEventInit<T>) {
57
+ super(type, params);
58
+ this.detail = params?.detail as T;
59
+ }
60
+ }
61
+
62
+ (globalThis as any).CustomEvent = CustomEventPolyfill;
63
+ };
64
+
65
+ const toHex = (bytes: Uint8Array): string => {
66
+ let out = "";
67
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
68
+ return out;
69
+ };
70
+
71
+ const toNumber = (value: bigint | number): number => {
72
+ return typeof value === "bigint" ? Number(value) : value;
73
+ };
74
+
75
+ export type SharedLogProxyLog = {
76
+ length: number;
77
+ get: (hash: string) => Promise<Entry<any> | undefined>;
78
+ has: (hash: string) => Promise<boolean>;
79
+ getHeads: () => LogResultsIterator<Entry<any>>;
80
+ toArray: () => Promise<Entry<any>[]>;
81
+ blocks: {
82
+ has: (hash: string) => Promise<boolean>;
83
+ };
84
+ };
85
+
86
+ export type SharedLogProxyReplicationIndex = {
87
+ iterate: (
88
+ request?: IterateOptions,
89
+ ) => import("@peerbit/indexer-interface").IndexIterator<
90
+ ReplicationRangeIndexable<any>,
91
+ undefined
92
+ >;
93
+ count: (request?: CountOptions) => Promise<number>;
94
+ getSize?: () => Promise<number>;
95
+ };
96
+
97
+ export type SharedLogProxy = SharedLogLike<any> & {
98
+ raw: SharedLogService;
99
+ log: SharedLogProxyLog;
100
+ replicationIndex: SharedLogProxyReplicationIndex;
101
+ node: { identity: { publicKey: PublicSignKey } };
102
+ close: () => Promise<void>;
103
+ };
104
+
105
+ const decodeEntry = (bytes: SharedLogBytes): Entry<any> => {
106
+ const entry = deserialize(bytes.value, Entry) as Entry<any>;
107
+ return entry.init({ encoding: NO_ENCODING });
108
+ };
109
+
110
+ const toReplicationRange = (
111
+ value: FixedReplicationOptions,
112
+ ): SharedLogReplicationRange => {
113
+ const factor = value.factor;
114
+ if (factor === "all" || factor === "right") {
115
+ return new SharedLogReplicationRange({
116
+ id: value.id,
117
+ factorMode: factor,
118
+ offset: value.offset as number | undefined,
119
+ normalized: value.normalized,
120
+ strict: value.strict,
121
+ });
122
+ }
123
+ if (typeof factor === "string") {
124
+ throw new Error(`Unsupported replication factor '${factor}'`);
125
+ }
126
+ if (typeof factor === "bigint") {
127
+ return new SharedLogReplicationRange({
128
+ id: value.id,
129
+ factor: Number(factor),
130
+ offset: value.offset != null ? Number(value.offset) : undefined,
131
+ normalized: value.normalized,
132
+ strict: value.strict,
133
+ });
134
+ }
135
+ return new SharedLogReplicationRange({
136
+ id: value.id,
137
+ factor: factor,
138
+ offset: value.offset as number | undefined,
139
+ normalized: value.normalized,
140
+ strict: value.strict,
141
+ });
142
+ };
143
+
144
+ const toReplicateValue = (
145
+ input: ReplicationOptions<any> | undefined,
146
+ ): SharedLogReplicateValue | undefined => {
147
+ if (input === undefined) return undefined;
148
+ if (typeof input === "boolean") {
149
+ return new SharedLogReplicateBool(input);
150
+ }
151
+ if (typeof input === "number") {
152
+ return new SharedLogReplicateFactor(input);
153
+ }
154
+ if (Array.isArray(input)) {
155
+ return new SharedLogReplicateFixedList(
156
+ input.map((range) =>
157
+ toReplicationRange(range as FixedReplicationOptions),
158
+ ),
159
+ );
160
+ }
161
+ if ((input as any)?.type === "resume") {
162
+ return toReplicateValue((input as any).default as any);
163
+ }
164
+ const fixed = input as FixedReplicationOptions;
165
+ if (fixed.factor == null && (fixed as any).factorMode == null) {
166
+ throw new Error("Replication options missing factor");
167
+ }
168
+ return new SharedLogReplicateFixed(toReplicationRange(fixed));
169
+ };
170
+
171
+ export const createSharedLogProxyFromService = async (
172
+ raw: SharedLogService,
173
+ ): Promise<SharedLogProxy> => {
174
+ ensureCustomEvent();
175
+
176
+ const randomId = (): string => {
177
+ if (
178
+ typeof crypto !== "undefined" &&
179
+ typeof crypto.randomUUID === "function"
180
+ ) {
181
+ return crypto.randomUUID();
182
+ }
183
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
184
+ };
185
+
186
+ const abortError = () => new Error("AbortError");
187
+
188
+ const resolution = (await raw.resolution()) as "u32" | "u64";
189
+ const decodeRange = (
190
+ bytes: SharedLogBytes,
191
+ ): ReplicationRangeIndexable<any> => {
192
+ if (resolution === "u32") {
193
+ return deserialize(
194
+ bytes.value,
195
+ ReplicationRangeIndexableU32,
196
+ ) as ReplicationRangeIndexable<any>;
197
+ }
198
+ return deserialize(
199
+ bytes.value,
200
+ ReplicationRangeIndexableU64,
201
+ ) as ReplicationRangeIndexable<any>;
202
+ };
203
+
204
+ const publicKey = await raw.publicKey();
205
+ const node = { identity: { publicKey } };
206
+
207
+ let length = toNumber(await raw.logLength());
208
+ let closed = false;
209
+
210
+ const events = new EventTarget();
211
+ const eventTypes = [
212
+ "join",
213
+ "leave",
214
+ "replicator:join",
215
+ "replicator:leave",
216
+ "replicator:mature",
217
+ "replication:change",
218
+ ];
219
+ const eventHandlers: Array<() => void> = [];
220
+
221
+ for (const type of eventTypes) {
222
+ const handler = (event: any) => {
223
+ const detail = (event?.detail as SharedLogEvent | undefined)?.publicKey;
224
+ if (!detail) return;
225
+ const payload =
226
+ type === "join" || type === "leave" ? detail : { publicKey: detail };
227
+ events.dispatchEvent(new CustomEvent(type, { detail: payload }));
228
+ };
229
+ raw.events.addEventListener(type, handler as any);
230
+ eventHandlers.push(() =>
231
+ raw.events.removeEventListener(type, handler as any),
232
+ );
233
+ }
234
+
235
+ const createEntriesIterator = (
236
+ service: SharedLogEntriesIteratorService,
237
+ ): LogResultsIterator<Entry<any>> => {
238
+ let done = false;
239
+ const iterator: LogResultsIterator<Entry<any>> = {
240
+ next: async (amount: number) => {
241
+ const batch = (await service.next(amount)) as SharedLogEntriesBatch;
242
+ done = batch.done;
243
+ return (batch.entries ?? []).map(decodeEntry);
244
+ },
245
+ done: () => done,
246
+ all: async () => {
247
+ const out: Entry<any>[] = [];
248
+ while (!done) {
249
+ const next = await iterator.next(10);
250
+ out.push(...next);
251
+ }
252
+ return out;
253
+ },
254
+ close: async () => {
255
+ done = true;
256
+ await service.close();
257
+ },
258
+ };
259
+ return iterator;
260
+ };
261
+
262
+ const log: SharedLogProxyLog = {
263
+ get: async (hash) => {
264
+ const bytes = await raw.logGet(hash);
265
+ return bytes ? decodeEntry(bytes) : undefined;
266
+ },
267
+ has: async (hash) => {
268
+ return raw.logHas(hash);
269
+ },
270
+ getHeads: () => {
271
+ const servicePromise = raw.logGetHeads();
272
+ let iterator: LogResultsIterator<Entry<any>>;
273
+ iterator = {
274
+ next: async (amount: number) => {
275
+ const service = await servicePromise;
276
+ Object.assign(iterator, createEntriesIterator(service));
277
+ return iterator.next(amount);
278
+ },
279
+ done: () => false,
280
+ all: async () => {
281
+ const service = await servicePromise;
282
+ Object.assign(iterator, createEntriesIterator(service));
283
+ return iterator.all();
284
+ },
285
+ close: async () => {
286
+ const service = await servicePromise;
287
+ Object.assign(iterator, createEntriesIterator(service));
288
+ return iterator.close();
289
+ },
290
+ };
291
+ return iterator;
292
+ },
293
+ toArray: async () => {
294
+ const items = await raw.logToArray();
295
+ length = items.length;
296
+ return items.map(decodeEntry);
297
+ },
298
+ blocks: {
299
+ has: async (hash) => {
300
+ return raw.logBlockHas(hash);
301
+ },
302
+ },
303
+ get length() {
304
+ return length;
305
+ },
306
+ };
307
+
308
+ const createReplicationIterator = (
309
+ service: SharedLogReplicationIteratorService,
310
+ ) => {
311
+ let done = false;
312
+ const iterator: import("@peerbit/indexer-interface").IndexIterator<
313
+ ReplicationRangeIndexable<any>,
314
+ undefined
315
+ > = {
316
+ next: async (amount: number) => {
317
+ const batch = (await service.next(amount)) as SharedLogReplicationBatch;
318
+ done = batch.done;
319
+ return (batch.results ?? []).map((result) => ({
320
+ id: result.id as IdKey,
321
+ value: decodeRange(result.value),
322
+ }));
323
+ },
324
+ done: () => done,
325
+ all: async () => {
326
+ const out: Array<{ id: IdKey; value: ReplicationRangeIndexable<any> }> =
327
+ [];
328
+ while (!done) {
329
+ const next = await iterator.next(10);
330
+ out.push(...next);
331
+ }
332
+ return out as any;
333
+ },
334
+ pending: async () => {
335
+ const pending = await service.pending();
336
+ return pending != null ? Number(pending) : 0;
337
+ },
338
+ close: async () => {
339
+ done = true;
340
+ await service.close();
341
+ },
342
+ };
343
+ return iterator;
344
+ };
345
+
346
+ const replicationIndex: SharedLogProxyReplicationIndex = {
347
+ iterate: (request?: IterateOptions) => {
348
+ const query = toQuery(request?.query);
349
+ const sort = toSort(request?.sort);
350
+ const iterateRequest = new SharedLogReplicationIterateRequest({
351
+ query,
352
+ sort,
353
+ });
354
+ const servicePromise = raw.replicationIterate(iterateRequest);
355
+ let iterator: import("@peerbit/indexer-interface").IndexIterator<
356
+ ReplicationRangeIndexable<any>,
357
+ undefined
358
+ >;
359
+ iterator = {
360
+ next: async (amount: number) => {
361
+ const service = await servicePromise;
362
+ Object.assign(iterator, createReplicationIterator(service));
363
+ return iterator.next(amount);
364
+ },
365
+ done: () => false,
366
+ all: async () => {
367
+ const service = await servicePromise;
368
+ Object.assign(iterator, createReplicationIterator(service));
369
+ return iterator.all();
370
+ },
371
+ pending: async () => {
372
+ const service = await servicePromise;
373
+ Object.assign(iterator, createReplicationIterator(service));
374
+ return iterator.pending();
375
+ },
376
+ close: async () => {
377
+ const service = await servicePromise;
378
+ Object.assign(iterator, createReplicationIterator(service));
379
+ return iterator.close();
380
+ },
381
+ };
382
+ return iterator;
383
+ },
384
+ count: async (request?: CountOptions) => {
385
+ const query = toQuery(request?.query);
386
+ const count = await raw.replicationCount(
387
+ new SharedLogReplicationCountRequest({ query }),
388
+ );
389
+ return toNumber(count);
390
+ },
391
+ getSize: async () => {
392
+ const count = await raw.replicationCount(
393
+ new SharedLogReplicationCountRequest({ query: [] }),
394
+ );
395
+ return toNumber(count);
396
+ },
397
+ };
398
+
399
+ const getReplicators = async (): Promise<Set<string>> => {
400
+ const replicators = await raw.getReplicators();
401
+ return new Set(replicators);
402
+ };
403
+
404
+ const waitForReplicator = async (
405
+ publicKey: PublicSignKey,
406
+ options?: {
407
+ eager?: boolean;
408
+ roleAge?: number;
409
+ timeout?: number;
410
+ signal?: AbortSignal;
411
+ },
412
+ ) => {
413
+ if (options?.signal?.aborted) {
414
+ throw new Error("AbortError");
415
+ }
416
+ const signal = options?.signal;
417
+ const requestId = signal ? randomId() : undefined;
418
+ const request = new SharedLogWaitForReplicatorRequest({
419
+ publicKey,
420
+ eager: options?.eager,
421
+ timeoutMs: options?.timeout,
422
+ roleAgeMs: options?.roleAge,
423
+ requestId,
424
+ });
425
+ const call = raw.waitForReplicator(request);
426
+ if (!signal || !requestId) {
427
+ await call;
428
+ return;
429
+ }
430
+
431
+ return new Promise<void>((resolve, reject) => {
432
+ const cleanup = () => {
433
+ signal.removeEventListener("abort", onAbort);
434
+ };
435
+ const onAbort = () => {
436
+ cleanup();
437
+ void raw.cancelWait(requestId).catch(() => {});
438
+ reject(abortError());
439
+ };
440
+ if (signal.aborted) {
441
+ onAbort();
442
+ return;
443
+ }
444
+ signal.addEventListener("abort", onAbort, { once: true });
445
+ call.then(
446
+ () => {
447
+ cleanup();
448
+ resolve();
449
+ },
450
+ (error) => {
451
+ cleanup();
452
+ reject(error);
453
+ },
454
+ );
455
+ });
456
+ };
457
+
458
+ const waitForReplicators = async (options?: {
459
+ timeout?: number;
460
+ roleAge?: number;
461
+ coverageThreshold?: number;
462
+ waitForNewPeers?: boolean;
463
+ signal?: AbortSignal;
464
+ }) => {
465
+ if (options?.signal?.aborted) {
466
+ throw new Error("AbortError");
467
+ }
468
+ const signal = options?.signal;
469
+ const requestId = signal ? randomId() : undefined;
470
+ const request = new SharedLogWaitForReplicatorsRequest({
471
+ timeoutMs: options?.timeout,
472
+ roleAgeMs: options?.roleAge,
473
+ coverageThreshold: options?.coverageThreshold,
474
+ waitForNewPeers: options?.waitForNewPeers,
475
+ requestId,
476
+ });
477
+ const call = raw.waitForReplicators(request);
478
+ if (!signal || !requestId) {
479
+ await call;
480
+ return;
481
+ }
482
+
483
+ return new Promise<void>((resolve, reject) => {
484
+ const cleanup = () => {
485
+ signal.removeEventListener("abort", onAbort);
486
+ };
487
+ const onAbort = () => {
488
+ cleanup();
489
+ void raw.cancelWait(requestId).catch(() => {});
490
+ reject(abortError());
491
+ };
492
+ if (signal.aborted) {
493
+ onAbort();
494
+ return;
495
+ }
496
+ signal.addEventListener("abort", onAbort, { once: true });
497
+ call.then(
498
+ () => {
499
+ cleanup();
500
+ resolve();
501
+ },
502
+ (error) => {
503
+ cleanup();
504
+ reject(error);
505
+ },
506
+ );
507
+ });
508
+ };
509
+
510
+ const replicate = async (
511
+ input?: ReplicationOptions<any>,
512
+ options?: {
513
+ reset?: boolean;
514
+ checkDuplicates?: boolean;
515
+ rebalance?: boolean;
516
+ mergeSegments?: boolean;
517
+ },
518
+ ) => {
519
+ const value = toReplicateValue(input);
520
+ const request =
521
+ value || options
522
+ ? new SharedLogReplicateRequest({
523
+ value,
524
+ reset: options?.reset,
525
+ checkDuplicates: options?.checkDuplicates,
526
+ rebalance: options?.rebalance,
527
+ mergeSegments: options?.mergeSegments,
528
+ })
529
+ : undefined;
530
+ await raw.replicate(request);
531
+ };
532
+
533
+ const unreplicate = async (ranges?: { id: Uint8Array }[]) => {
534
+ const request = ranges
535
+ ? new SharedLogUnreplicateRequest({ ids: ranges.map((r) => r.id) })
536
+ : undefined;
537
+ await raw.unreplicate(request);
538
+ };
539
+
540
+ const calculateCoverage = async (options?: {
541
+ start?: number | bigint;
542
+ end?: number | bigint;
543
+ roleAge?: number;
544
+ }) => {
545
+ const request = options
546
+ ? new SharedLogCoverageRequest({
547
+ start: options.start != null ? Number(options.start) : undefined,
548
+ end: options.end != null ? Number(options.end) : undefined,
549
+ roleAgeMs: options.roleAge,
550
+ })
551
+ : undefined;
552
+ return raw.calculateCoverage(request);
553
+ };
554
+
555
+ const getMyReplicationSegments = async () => {
556
+ const ranges = await raw.getMyReplicationSegments();
557
+ return ranges.map(decodeRange);
558
+ };
559
+
560
+ const getAllReplicationSegments = async () => {
561
+ const ranges = await raw.getAllReplicationSegments();
562
+ return ranges.map(decodeRange);
563
+ };
564
+
565
+ const close = async () => {
566
+ if (closed) return;
567
+ closed = true;
568
+ for (const off of eventHandlers) off();
569
+ await raw.close();
570
+ };
571
+
572
+ const proxy = {
573
+ raw,
574
+ node,
575
+ events,
576
+ log,
577
+ replicationIndex,
578
+ getReplicators,
579
+ waitForReplicator,
580
+ waitForReplicators,
581
+ replicate,
582
+ unreplicate,
583
+ calculateCoverage,
584
+ getMyReplicationSegments,
585
+ getAllReplicationSegments,
586
+ close,
587
+ } as SharedLogProxy;
588
+
589
+ Object.defineProperty(proxy, "closed", {
590
+ get: () => closed,
591
+ set: (value: boolean) => {
592
+ closed = value;
593
+ },
594
+ enumerable: true,
595
+ });
596
+
597
+ return proxy;
598
+ };
599
+
600
+ export const openSharedLog = async (properties: {
601
+ client: CanonicalClient;
602
+ id: Uint8Array;
603
+ }): Promise<SharedLogProxy> => {
604
+ const channel = await properties.client.openPort(
605
+ "@peerbit/shared-log",
606
+ serialize(new OpenSharedLogRequest({ id: properties.id })),
607
+ );
608
+
609
+ const transport = createMessagePortTransport(channel, {
610
+ requestTimeoutMs: (method) => {
611
+ if (method === "waitForReplicator" || method === "waitForReplicators") {
612
+ return undefined;
613
+ }
614
+ return 30_000;
615
+ },
616
+ });
617
+ const raw = createProxyFromService(
618
+ SharedLogService,
619
+ transport,
620
+ ) as unknown as SharedLogService;
621
+
622
+ const proxy = await createSharedLogProxyFromService(raw);
623
+ const rawClose = proxy.close.bind(proxy);
624
+ proxy.close = async () => {
625
+ try {
626
+ await rawClose();
627
+ } finally {
628
+ channel.close?.();
629
+ }
630
+ };
631
+ return proxy;
632
+ };
633
+
634
+ export const createSharedLogCacheKey = (
635
+ logId?: Uint8Array,
636
+ ): string | undefined => {
637
+ if (!logId) return undefined;
638
+ return toHex(logId);
639
+ };