tablinum 0.4.0 → 0.6.0
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/dist/crud/collection-handle.d.ts +2 -1
- package/dist/db/create-tablinum.d.ts +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1383 -907
- package/dist/services/Config.d.ts +2 -0
- package/dist/storage/idb.d.ts +0 -1
- package/dist/svelte/collection.svelte.d.ts +2 -1
- package/dist/svelte/index.svelte.js +1458 -952
- package/dist/sync/compact-event.d.ts +3 -0
- package/dist/sync/sync-service.d.ts +4 -1
- package/package.json +8 -27
- package/README.md +0 -238
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
2
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
3
|
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined")
|
|
5
|
-
return require.apply(this, arguments);
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
5
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
6
|
});
|
|
8
7
|
|
|
9
8
|
// src/schema/field.ts
|
|
10
9
|
function make(kind, isOptional, isArray, fields) {
|
|
11
|
-
return { _tag: "FieldDef", kind, isOptional, isArray, ...fields !==
|
|
10
|
+
return { _tag: "FieldDef", kind, isOptional, isArray, ...fields !== void 0 && { fields } };
|
|
12
11
|
}
|
|
13
12
|
var field = {
|
|
14
13
|
string: () => make("string", false, false),
|
|
@@ -19,8 +18,9 @@ var field = {
|
|
|
19
18
|
optional: (inner) => make(inner.kind, true, inner.isArray, inner.fields),
|
|
20
19
|
array: (inner) => make(inner.kind, inner.isOptional, true, inner.fields)
|
|
21
20
|
};
|
|
21
|
+
|
|
22
22
|
// src/schema/collection.ts
|
|
23
|
-
var RESERVED_NAMES = new Set(["id", "_d", "_u", "_e", "_a"]);
|
|
23
|
+
var RESERVED_NAMES = /* @__PURE__ */ new Set(["id", "_d", "_u", "_e", "_a"]);
|
|
24
24
|
function collection(name, fields, options) {
|
|
25
25
|
if (!name || name.trim().length === 0) {
|
|
26
26
|
throw new Error("Collection name must not be empty");
|
|
@@ -42,7 +42,9 @@ function collection(name, fields, options) {
|
|
|
42
42
|
throw new Error(`Index field "${idx}" does not exist in collection "${name}"`);
|
|
43
43
|
}
|
|
44
44
|
if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
|
|
45
|
-
throw new Error(
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Field "${idx}" cannot be indexed (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`
|
|
47
|
+
);
|
|
46
48
|
}
|
|
47
49
|
indices.push(idx);
|
|
48
50
|
}
|
|
@@ -53,6 +55,7 @@ function collection(name, fields, options) {
|
|
|
53
55
|
}
|
|
54
56
|
return { _tag: "CollectionDef", name, fields, indices, eventRetention };
|
|
55
57
|
}
|
|
58
|
+
|
|
56
59
|
// src/db/invite.ts
|
|
57
60
|
import { Schema as Schema2 } from "effect";
|
|
58
61
|
|
|
@@ -82,15 +85,17 @@ var PersistedEpochStoreSchema = Schema.Struct({
|
|
|
82
85
|
epochs: Schema.Array(PersistedEpochSchema),
|
|
83
86
|
currentEpochId: Schema.String
|
|
84
87
|
});
|
|
85
|
-
var decodePersistedEpochStore = Schema.decodeUnknownSync(
|
|
88
|
+
var decodePersistedEpochStore = Schema.decodeUnknownSync(
|
|
89
|
+
Schema.fromJsonString(PersistedEpochStoreSchema)
|
|
90
|
+
);
|
|
86
91
|
function createEpochKey(id, privateKeyHex, createdBy, parentEpoch) {
|
|
87
92
|
const publicKey = getPublicKey(hexToBytes(privateKeyHex));
|
|
88
93
|
const base = { id, privateKey: privateKeyHex, publicKey, createdBy };
|
|
89
|
-
return parentEpoch !==
|
|
94
|
+
return parentEpoch !== void 0 ? { ...base, parentEpoch } : base;
|
|
90
95
|
}
|
|
91
96
|
function createEpochStore(initialEpoch) {
|
|
92
|
-
const epochs = new Map;
|
|
93
|
-
const keysByPublicKey = new Map;
|
|
97
|
+
const epochs = /* @__PURE__ */ new Map();
|
|
98
|
+
const keysByPublicKey = /* @__PURE__ */ new Map();
|
|
94
99
|
epochs.set(initialEpoch.id, initialEpoch);
|
|
95
100
|
keysByPublicKey.set(initialEpoch.publicKey, hexToBytes(initialEpoch.privateKey));
|
|
96
101
|
return { epochs, keysByPublicKey, currentEpochId: initialEpoch.id };
|
|
@@ -100,7 +105,14 @@ function addEpoch(store, epoch) {
|
|
|
100
105
|
store.keysByPublicKey.set(epoch.publicKey, hexToBytes(epoch.privateKey));
|
|
101
106
|
}
|
|
102
107
|
function hydrateEpochStore(snapshot) {
|
|
103
|
-
const [firstEpoch, ...remainingEpochs] = snapshot.epochs.map(
|
|
108
|
+
const [firstEpoch, ...remainingEpochs] = snapshot.epochs.map(
|
|
109
|
+
(epoch) => createEpochKey(
|
|
110
|
+
EpochId(epoch.id),
|
|
111
|
+
epoch.privateKey,
|
|
112
|
+
epoch.createdBy,
|
|
113
|
+
epoch.parentEpoch !== void 0 ? EpochId(epoch.parentEpoch) : void 0
|
|
114
|
+
)
|
|
115
|
+
);
|
|
104
116
|
if (!firstEpoch) {
|
|
105
117
|
throw new Error("Epoch snapshot must contain at least one epoch");
|
|
106
118
|
}
|
|
@@ -116,9 +128,16 @@ function createEpochStoreFromInputs(epochKeys, options = {}) {
|
|
|
116
128
|
throw new Error("Epoch input must contain at least one key");
|
|
117
129
|
}
|
|
118
130
|
const createdBy = options.createdBy ?? "";
|
|
119
|
-
const epochs = epochKeys.map(
|
|
131
|
+
const epochs = epochKeys.map(
|
|
132
|
+
(epochKey, index) => createEpochKey(
|
|
133
|
+
epochKey.epochId,
|
|
134
|
+
epochKey.key,
|
|
135
|
+
createdBy,
|
|
136
|
+
index > 0 ? epochKeys[index - 1].epochId : void 0
|
|
137
|
+
)
|
|
138
|
+
);
|
|
120
139
|
const store = createEpochStore(epochs[0]);
|
|
121
|
-
for (let i = 1;i < epochs.length; i++) {
|
|
140
|
+
for (let i = 1; i < epochs.length; i++) {
|
|
122
141
|
addEpoch(store, epochs[i]);
|
|
123
142
|
}
|
|
124
143
|
store.currentEpochId = epochs[epochs.length - 1].id;
|
|
@@ -145,7 +164,7 @@ function serializeEpochStore(store) {
|
|
|
145
164
|
id: epoch.id,
|
|
146
165
|
privateKey: epoch.privateKey,
|
|
147
166
|
createdBy: epoch.createdBy,
|
|
148
|
-
...epoch.parentEpoch !==
|
|
167
|
+
...epoch.parentEpoch !== void 0 ? { parentEpoch: epoch.parentEpoch } : {}
|
|
149
168
|
})),
|
|
150
169
|
currentEpochId: store.currentEpochId
|
|
151
170
|
};
|
|
@@ -193,34 +212,29 @@ function decodeInvite(encoded) {
|
|
|
193
212
|
throw new Error("Invalid invite: unexpected shape");
|
|
194
213
|
}
|
|
195
214
|
}
|
|
215
|
+
|
|
196
216
|
// src/errors.ts
|
|
197
217
|
import { Data } from "effect";
|
|
218
|
+
var ValidationError = class extends Data.TaggedError("ValidationError") {
|
|
219
|
+
};
|
|
220
|
+
var StorageError = class extends Data.TaggedError("StorageError") {
|
|
221
|
+
};
|
|
222
|
+
var CryptoError = class extends Data.TaggedError("CryptoError") {
|
|
223
|
+
};
|
|
224
|
+
var RelayError = class extends Data.TaggedError("RelayError") {
|
|
225
|
+
};
|
|
226
|
+
var SyncError = class extends Data.TaggedError("SyncError") {
|
|
227
|
+
};
|
|
228
|
+
var NotFoundError = class extends Data.TaggedError("NotFoundError") {
|
|
229
|
+
};
|
|
230
|
+
var ClosedError = class extends Data.TaggedError("ClosedError") {
|
|
231
|
+
};
|
|
198
232
|
|
|
199
|
-
class ValidationError extends Data.TaggedError("ValidationError") {
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
class StorageError extends Data.TaggedError("StorageError") {
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
class CryptoError extends Data.TaggedError("CryptoError") {
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
class RelayError extends Data.TaggedError("RelayError") {
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
class SyncError extends Data.TaggedError("SyncError") {
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
class NotFoundError extends Data.TaggedError("NotFoundError") {
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
class ClosedError extends Data.TaggedError("ClosedError") {
|
|
218
|
-
}
|
|
219
233
|
// src/svelte/tablinum.svelte.ts
|
|
220
|
-
import { Effect as Effect25, Exit as Exit2, Scope as Scope6 } from "effect";
|
|
234
|
+
import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
|
|
221
235
|
|
|
222
236
|
// src/db/create-tablinum.ts
|
|
223
|
-
import { Effect as Effect22, Layer as
|
|
237
|
+
import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
|
|
224
238
|
|
|
225
239
|
// src/db/runtime-config.ts
|
|
226
240
|
import { Effect, Schema as Schema3 } from "effect";
|
|
@@ -232,49 +246,61 @@ var RuntimeConfigSchema = Schema3.Struct({
|
|
|
232
246
|
epochKeys: Schema3.optional(Schema3.Array(EpochKeyInputSchema))
|
|
233
247
|
});
|
|
234
248
|
function resolveRuntimeConfig(source) {
|
|
235
|
-
return Schema3.decodeUnknownEffect(RuntimeConfigSchema)(source).pipe(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
249
|
+
return Schema3.decodeUnknownEffect(RuntimeConfigSchema)(source).pipe(
|
|
250
|
+
Effect.map((config) => ({
|
|
251
|
+
relays: [...config.relays],
|
|
252
|
+
privateKey: config.privateKey,
|
|
253
|
+
epochKeys: config.epochKeys?.map((ek) => ({ epochId: EpochId(ek.epochId), key: ek.key })),
|
|
254
|
+
dbName: DatabaseName(config.dbName ?? "tablinum")
|
|
255
|
+
})),
|
|
256
|
+
Effect.mapError(
|
|
257
|
+
(error) => new ValidationError({
|
|
258
|
+
message: `Invalid Tablinum configuration: ${error.message}`
|
|
259
|
+
})
|
|
260
|
+
)
|
|
261
|
+
);
|
|
243
262
|
}
|
|
244
263
|
|
|
245
264
|
// src/services/Config.ts
|
|
246
265
|
import { ServiceMap } from "effect";
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
266
|
+
var Config = class extends ServiceMap.Service()("tablinum/Config") {
|
|
267
|
+
};
|
|
250
268
|
|
|
251
269
|
// src/services/Tablinum.ts
|
|
252
270
|
import { ServiceMap as ServiceMap2 } from "effect";
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
271
|
+
var Tablinum = class extends ServiceMap2.Service()(
|
|
272
|
+
"tablinum/Tablinum"
|
|
273
|
+
) {
|
|
274
|
+
};
|
|
256
275
|
|
|
257
276
|
// src/layers/TablinumLive.ts
|
|
258
|
-
import { Effect as Effect21, Exit, Layer as
|
|
277
|
+
import { Effect as Effect21, Exit, Layer as Layer9, Option as Option9, PubSub as PubSub2, References as References3, Ref as Ref5, Scope as Scope4 } from "effect";
|
|
259
278
|
|
|
260
279
|
// src/crud/watch.ts
|
|
261
280
|
import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
|
|
262
|
-
function watchCollection(ctx, storage, collectionName, filter,
|
|
281
|
+
function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
|
|
263
282
|
const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
|
|
264
283
|
const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
|
|
265
|
-
return
|
|
284
|
+
return mapRecord2 ? filtered.map(mapRecord2) : filtered;
|
|
266
285
|
});
|
|
267
|
-
const changes = Stream.fromPubSub(ctx.pubsub).pipe(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
286
|
+
const changes = Stream.fromPubSub(ctx.pubsub).pipe(
|
|
287
|
+
Stream.filter((event) => event.collection === collectionName),
|
|
288
|
+
Stream.mapEffect(
|
|
289
|
+
() => Effect2.gen(function* () {
|
|
290
|
+
const replaying = yield* Ref.get(ctx.replayingRef);
|
|
291
|
+
if (replaying) return void 0;
|
|
292
|
+
return yield* query();
|
|
293
|
+
})
|
|
294
|
+
),
|
|
295
|
+
Stream.filter((result) => result !== void 0)
|
|
296
|
+
);
|
|
297
|
+
return Stream.unwrap(
|
|
298
|
+
Effect2.gen(function* () {
|
|
299
|
+
yield* Effect2.sleep(0);
|
|
300
|
+
const initial = yield* query();
|
|
301
|
+
return Stream.concat(Stream.make(initial), changes);
|
|
302
|
+
})
|
|
303
|
+
);
|
|
278
304
|
}
|
|
279
305
|
function notifyChange(ctx, event) {
|
|
280
306
|
return PubSub.publish(ctx.pubsub, event).pipe(Effect2.asVoid);
|
|
@@ -297,12 +323,9 @@ import { Effect as Effect3 } from "effect";
|
|
|
297
323
|
|
|
298
324
|
// src/storage/lww.ts
|
|
299
325
|
function resolveWinner(existing, incoming) {
|
|
300
|
-
if (existing === null)
|
|
301
|
-
|
|
302
|
-
if (incoming.createdAt
|
|
303
|
-
return incoming;
|
|
304
|
-
if (incoming.createdAt < existing.createdAt)
|
|
305
|
-
return existing;
|
|
326
|
+
if (existing === null) return incoming;
|
|
327
|
+
if (incoming.createdAt > existing.createdAt) return incoming;
|
|
328
|
+
if (incoming.createdAt < existing.createdAt) return existing;
|
|
306
329
|
return incoming.id < existing.id ? incoming : existing;
|
|
307
330
|
}
|
|
308
331
|
|
|
@@ -369,8 +392,7 @@ function applyEvent(storage, event) {
|
|
|
369
392
|
};
|
|
370
393
|
const incomingMeta = { id: event.id, createdAt: event.createdAt };
|
|
371
394
|
const winner = resolveWinner(existingMeta, incomingMeta);
|
|
372
|
-
if (winner.id !== event.id)
|
|
373
|
-
return false;
|
|
395
|
+
if (winner.id !== event.id) return false;
|
|
374
396
|
}
|
|
375
397
|
if (existing && event.kind === "u") {
|
|
376
398
|
yield* storage.putRecord(event.collection, deepMerge(existing, buildRecord(event)));
|
|
@@ -386,10 +408,11 @@ function rebuild(storage, collections) {
|
|
|
386
408
|
yield* storage.clearRecords(col);
|
|
387
409
|
}
|
|
388
410
|
const allEvents = yield* storage.getAllEvents();
|
|
389
|
-
const sorted = [...allEvents].sort(
|
|
411
|
+
const sorted = [...allEvents].sort(
|
|
412
|
+
(a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1)
|
|
413
|
+
);
|
|
390
414
|
for (const event of sorted) {
|
|
391
|
-
if (event.data === null && event.kind !== "d")
|
|
392
|
-
continue;
|
|
415
|
+
if (event.data === null && event.kind !== "d") continue;
|
|
393
416
|
yield* applyEvent(storage, event);
|
|
394
417
|
}
|
|
395
418
|
});
|
|
@@ -442,9 +465,16 @@ function buildStructSchema(def, options = {}) {
|
|
|
442
465
|
}
|
|
443
466
|
function buildValidator(collectionName, def) {
|
|
444
467
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
445
|
-
return (input) => decode(input).pipe(
|
|
446
|
-
|
|
447
|
-
|
|
468
|
+
return (input) => decode(input).pipe(
|
|
469
|
+
Effect4.map(
|
|
470
|
+
(result) => result
|
|
471
|
+
),
|
|
472
|
+
Effect4.mapError(
|
|
473
|
+
(e) => new ValidationError({
|
|
474
|
+
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
475
|
+
})
|
|
476
|
+
)
|
|
477
|
+
);
|
|
448
478
|
}
|
|
449
479
|
function buildPartialValidator(collectionName, def) {
|
|
450
480
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
@@ -456,35 +486,38 @@ function buildPartialValidator(collectionName, def) {
|
|
|
456
486
|
}
|
|
457
487
|
const record = input;
|
|
458
488
|
const unknownField = Object.keys(record).find((key) => !Object.hasOwn(def.fields, key));
|
|
459
|
-
if (unknownField !==
|
|
489
|
+
if (unknownField !== void 0) {
|
|
460
490
|
return yield* new ValidationError({
|
|
461
491
|
message: `Unknown field "${unknownField}" in collection "${collectionName}"`,
|
|
462
492
|
field: unknownField
|
|
463
493
|
});
|
|
464
494
|
}
|
|
465
|
-
return yield* decode(record).pipe(
|
|
466
|
-
|
|
467
|
-
|
|
495
|
+
return yield* decode(record).pipe(
|
|
496
|
+
Effect4.map((result) => result),
|
|
497
|
+
Effect4.mapError(
|
|
498
|
+
(e) => new ValidationError({
|
|
499
|
+
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
500
|
+
})
|
|
501
|
+
)
|
|
502
|
+
);
|
|
468
503
|
});
|
|
469
504
|
}
|
|
470
505
|
|
|
471
506
|
// src/crud/collection-handle.ts
|
|
472
|
-
import { Effect as Effect6, Option as Option3 } from "effect";
|
|
507
|
+
import { Effect as Effect6, Option as Option3, References } from "effect";
|
|
473
508
|
|
|
474
509
|
// src/utils/uuid.ts
|
|
475
510
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
476
511
|
function toBase64url(bytes) {
|
|
477
512
|
let result = "";
|
|
478
|
-
for (let i = 0;i < bytes.length; i += 3) {
|
|
513
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
479
514
|
const b0 = bytes[i];
|
|
480
515
|
const b1 = bytes[i + 1] ?? 0;
|
|
481
516
|
const b2 = bytes[i + 2] ?? 0;
|
|
482
517
|
result += alphabet[b0 >> 2];
|
|
483
518
|
result += alphabet[(b0 & 3) << 4 | b1 >> 4];
|
|
484
|
-
if (i + 1 < bytes.length)
|
|
485
|
-
|
|
486
|
-
if (i + 2 < bytes.length)
|
|
487
|
-
result += alphabet[b2 & 63];
|
|
519
|
+
if (i + 1 < bytes.length) result += alphabet[(b1 & 15) << 2 | b2 >> 6];
|
|
520
|
+
if (i + 2 < bytes.length) result += alphabet[b2 & 63];
|
|
488
521
|
}
|
|
489
522
|
return result;
|
|
490
523
|
}
|
|
@@ -529,16 +562,28 @@ function executeQuery(ctx, plan) {
|
|
|
529
562
|
if (plan.indexQuery && ctx.def.indices.includes(plan.indexQuery.field)) {
|
|
530
563
|
if (plan.indexQuery.type === "value") {
|
|
531
564
|
results = [
|
|
532
|
-
...yield* ctx.storage.getByIndex(
|
|
565
|
+
...yield* ctx.storage.getByIndex(
|
|
566
|
+
ctx.collectionName,
|
|
567
|
+
plan.indexQuery.field,
|
|
568
|
+
plan.indexQuery.range
|
|
569
|
+
)
|
|
533
570
|
];
|
|
534
571
|
} else {
|
|
535
572
|
results = [
|
|
536
|
-
...yield* ctx.storage.getByIndexRange(
|
|
573
|
+
...yield* ctx.storage.getByIndexRange(
|
|
574
|
+
ctx.collectionName,
|
|
575
|
+
plan.indexQuery.field,
|
|
576
|
+
plan.indexQuery.range
|
|
577
|
+
)
|
|
537
578
|
];
|
|
538
579
|
}
|
|
539
580
|
} else if (plan.orderBy && ctx.def.indices.includes(plan.orderBy.field) && plan.filters.length === 0) {
|
|
540
581
|
results = [
|
|
541
|
-
...yield* ctx.storage.getAllSorted(
|
|
582
|
+
...yield* ctx.storage.getAllSorted(
|
|
583
|
+
ctx.collectionName,
|
|
584
|
+
plan.orderBy.field,
|
|
585
|
+
plan.orderBy.direction === "desc" ? "prev" : "next"
|
|
586
|
+
)
|
|
542
587
|
];
|
|
543
588
|
} else {
|
|
544
589
|
results = [...yield* ctx.storage.getAllRecords(ctx.collectionName)];
|
|
@@ -562,7 +607,7 @@ function executeQuery(ctx, plan) {
|
|
|
562
607
|
if (plan.offset) {
|
|
563
608
|
results = results.slice(plan.offset);
|
|
564
609
|
}
|
|
565
|
-
if (plan.limit !== null && plan.limit !==
|
|
610
|
+
if (plan.limit !== null && plan.limit !== void 0) {
|
|
566
611
|
results = results.slice(0, plan.limit);
|
|
567
612
|
}
|
|
568
613
|
return results.map(ctx.mapRecord);
|
|
@@ -570,16 +615,23 @@ function executeQuery(ctx, plan) {
|
|
|
570
615
|
}
|
|
571
616
|
function watchQuery(ctx, plan) {
|
|
572
617
|
const query = () => executeQuery(ctx, plan);
|
|
573
|
-
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
618
|
+
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
619
|
+
Stream2.filter((event) => event.collection === ctx.collectionName),
|
|
620
|
+
Stream2.mapEffect(
|
|
621
|
+
() => Effect5.gen(function* () {
|
|
622
|
+
const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
|
|
623
|
+
if (replaying) return void 0;
|
|
624
|
+
return yield* query();
|
|
625
|
+
})
|
|
626
|
+
),
|
|
627
|
+
Stream2.filter((result) => result !== void 0)
|
|
628
|
+
);
|
|
629
|
+
return Stream2.unwrap(
|
|
630
|
+
Effect5.gen(function* () {
|
|
631
|
+
const initial = yield* query();
|
|
632
|
+
return Stream2.concat(Stream2.make(initial), changes);
|
|
633
|
+
})
|
|
634
|
+
);
|
|
583
635
|
}
|
|
584
636
|
function makeQueryBuilder(ctx, plan) {
|
|
585
637
|
return {
|
|
@@ -601,15 +653,18 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
601
653
|
offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
|
|
602
654
|
limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
|
|
603
655
|
get: () => executeQuery(ctx, plan),
|
|
604
|
-
first: () => Effect5.map(
|
|
656
|
+
first: () => Effect5.map(
|
|
657
|
+
executeQuery(ctx, { ...plan, limit: 1 }),
|
|
658
|
+
(results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
|
|
659
|
+
),
|
|
605
660
|
count: () => Effect5.map(executeQuery(ctx, plan), (results) => results.length),
|
|
606
661
|
watch: () => watchQuery(ctx, plan)
|
|
607
662
|
};
|
|
608
663
|
}
|
|
609
|
-
function createWhereClause(storage, watchCtx, collectionName, def, fieldName,
|
|
610
|
-
const ctx = { storage, watchCtx, collectionName, def, mapRecord };
|
|
664
|
+
function createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord2) {
|
|
665
|
+
const ctx = { storage, watchCtx, collectionName, def, mapRecord: mapRecord2 };
|
|
611
666
|
const fieldDef = def.fields[fieldName];
|
|
612
|
-
const isIndexed = def.indices.includes(fieldName) && fieldDef !== null && fieldDef !==
|
|
667
|
+
const isIndexed = def.indices.includes(fieldName) && fieldDef !== null && fieldDef !== void 0 && fieldDef.kind !== "boolean";
|
|
613
668
|
const withFilter = (filterFn, indexQuery) => {
|
|
614
669
|
const plan = {
|
|
615
670
|
...emptyPlan(),
|
|
@@ -620,30 +675,51 @@ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, ma
|
|
|
620
675
|
return makeQueryBuilder(ctx, plan);
|
|
621
676
|
};
|
|
622
677
|
return {
|
|
623
|
-
equals: (value) => withFilter(
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
678
|
+
equals: (value) => withFilter(
|
|
679
|
+
(r) => r[fieldName] === value,
|
|
680
|
+
isIndexed ? { field: fieldName, range: value, type: "value" } : void 0
|
|
681
|
+
),
|
|
682
|
+
above: (value) => withFilter(
|
|
683
|
+
(r) => r[fieldName] > value,
|
|
684
|
+
isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, true), type: "range" } : void 0
|
|
685
|
+
),
|
|
686
|
+
aboveOrEqual: (value) => withFilter(
|
|
687
|
+
(r) => r[fieldName] >= value,
|
|
688
|
+
isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, false), type: "range" } : void 0
|
|
689
|
+
),
|
|
690
|
+
below: (value) => withFilter(
|
|
691
|
+
(r) => r[fieldName] < value,
|
|
692
|
+
isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, true), type: "range" } : void 0
|
|
693
|
+
),
|
|
694
|
+
belowOrEqual: (value) => withFilter(
|
|
695
|
+
(r) => r[fieldName] <= value,
|
|
696
|
+
isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, false), type: "range" } : void 0
|
|
697
|
+
),
|
|
628
698
|
between: (lower, upper, options) => {
|
|
629
699
|
const includeLower = options?.includeLower ?? true;
|
|
630
700
|
const includeUpper = options?.includeUpper ?? true;
|
|
631
|
-
return withFilter(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
701
|
+
return withFilter(
|
|
702
|
+
(r) => {
|
|
703
|
+
const v = r[fieldName];
|
|
704
|
+
const aboveLower = includeLower ? v >= lower : v > lower;
|
|
705
|
+
const belowUpper = includeUpper ? v <= upper : v < upper;
|
|
706
|
+
return aboveLower && belowUpper;
|
|
707
|
+
},
|
|
708
|
+
isIndexed ? {
|
|
709
|
+
field: fieldName,
|
|
710
|
+
range: IDBKeyRange.bound(lower, upper, !includeLower, !includeUpper),
|
|
711
|
+
type: "range"
|
|
712
|
+
} : void 0
|
|
713
|
+
);
|
|
714
|
+
},
|
|
715
|
+
startsWith: (prefix) => withFilter(
|
|
716
|
+
(r) => typeof r[fieldName] === "string" && r[fieldName].startsWith(prefix),
|
|
717
|
+
isIndexed ? {
|
|
637
718
|
field: fieldName,
|
|
638
|
-
range: IDBKeyRange.bound(
|
|
719
|
+
range: IDBKeyRange.bound(prefix, prefix + "\uFFFF", false, false),
|
|
639
720
|
type: "range"
|
|
640
|
-
} :
|
|
641
|
-
|
|
642
|
-
startsWith: (prefix) => withFilter((r) => typeof r[fieldName] === "string" && r[fieldName].startsWith(prefix), isIndexed ? {
|
|
643
|
-
field: fieldName,
|
|
644
|
-
range: IDBKeyRange.bound(prefix, prefix + "", false, false),
|
|
645
|
-
type: "range"
|
|
646
|
-
} : undefined),
|
|
721
|
+
} : void 0
|
|
722
|
+
),
|
|
647
723
|
anyOf: (values) => {
|
|
648
724
|
const set = new Set(values);
|
|
649
725
|
return withFilter((r) => set.has(r[fieldName]));
|
|
@@ -654,8 +730,8 @@ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, ma
|
|
|
654
730
|
}
|
|
655
731
|
};
|
|
656
732
|
}
|
|
657
|
-
function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName,
|
|
658
|
-
const ctx = { storage, watchCtx, collectionName, def, mapRecord };
|
|
733
|
+
function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord2) {
|
|
734
|
+
const ctx = { storage, watchCtx, collectionName, def, mapRecord: mapRecord2 };
|
|
659
735
|
const plan = {
|
|
660
736
|
...emptyPlan(),
|
|
661
737
|
orderBy: { field: fieldName, direction: "asc" }
|
|
@@ -680,7 +756,7 @@ function replayState(recordId, events, stopAtId) {
|
|
|
680
756
|
state = deepMerge(state, e.data);
|
|
681
757
|
}
|
|
682
758
|
}
|
|
683
|
-
if (stopAtId !==
|
|
759
|
+
if (stopAtId !== void 0 && e.id === stopAtId) {
|
|
684
760
|
break;
|
|
685
761
|
}
|
|
686
762
|
}
|
|
@@ -698,8 +774,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
|
698
774
|
function pruneEvents(storage, collection2, recordId, retention) {
|
|
699
775
|
return Effect6.gen(function* () {
|
|
700
776
|
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
701
|
-
if (events.length <= retention)
|
|
702
|
-
return;
|
|
777
|
+
if (events.length <= retention) return;
|
|
703
778
|
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
704
779
|
const retained = sorted.slice(0, retention);
|
|
705
780
|
const toStrip = sorted.slice(retention);
|
|
@@ -708,8 +783,7 @@ function pruneEvents(storage, collection2, recordId, retention) {
|
|
|
708
783
|
yield* promoteToSnapshot(storage, collection2, recordId, oldestRetained, sorted);
|
|
709
784
|
}
|
|
710
785
|
for (const e of toStrip) {
|
|
711
|
-
if (e.data !== null)
|
|
712
|
-
yield* storage.stripEventData(e.id);
|
|
786
|
+
if (e.data !== null) yield* storage.stripEventData(e.id);
|
|
713
787
|
}
|
|
714
788
|
});
|
|
715
789
|
}
|
|
@@ -717,13 +791,13 @@ function mapRecord(record) {
|
|
|
717
791
|
const { _d, _u, _a, _e, ...fields } = record;
|
|
718
792
|
return fields;
|
|
719
793
|
}
|
|
720
|
-
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite) {
|
|
794
|
+
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
721
795
|
const collectionName = def.name;
|
|
796
|
+
const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
722
797
|
const commitEvent = (event) => Effect6.gen(function* () {
|
|
723
798
|
yield* storage.putEvent(event);
|
|
724
799
|
yield* applyEvent(storage, event);
|
|
725
|
-
if (onWrite)
|
|
726
|
-
yield* onWrite(event);
|
|
800
|
+
if (onWrite) yield* onWrite(event);
|
|
727
801
|
yield* notifyChange(watchCtx, {
|
|
728
802
|
collection: collectionName,
|
|
729
803
|
recordId: event.recordId,
|
|
@@ -731,67 +805,84 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
731
805
|
});
|
|
732
806
|
});
|
|
733
807
|
const handle = {
|
|
734
|
-
add: (data) =>
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
recordId: id,
|
|
742
|
-
kind: "c",
|
|
743
|
-
data: fullRecord,
|
|
744
|
-
createdAt: Date.now(),
|
|
745
|
-
author: localAuthor
|
|
746
|
-
};
|
|
747
|
-
yield* commitEvent(event);
|
|
748
|
-
return id;
|
|
749
|
-
}),
|
|
750
|
-
update: (id, data) => Effect6.gen(function* () {
|
|
751
|
-
const existing = yield* storage.getRecord(collectionName, id);
|
|
752
|
-
if (!existing || existing._d) {
|
|
753
|
-
return yield* new NotFoundError({
|
|
808
|
+
add: (data) => withLog(
|
|
809
|
+
Effect6.gen(function* () {
|
|
810
|
+
const id = uuidv7();
|
|
811
|
+
const fullRecord = { id, ...data };
|
|
812
|
+
yield* validator(fullRecord);
|
|
813
|
+
const event = {
|
|
814
|
+
id: makeEventId(),
|
|
754
815
|
collection: collectionName,
|
|
755
|
-
id
|
|
816
|
+
recordId: id,
|
|
817
|
+
kind: "c",
|
|
818
|
+
data: fullRecord,
|
|
819
|
+
createdAt: Date.now(),
|
|
820
|
+
author: localAuthor
|
|
821
|
+
};
|
|
822
|
+
yield* commitEvent(event);
|
|
823
|
+
yield* Effect6.logDebug("Record added", {
|
|
824
|
+
collection: collectionName,
|
|
825
|
+
recordId: id,
|
|
826
|
+
data: fullRecord
|
|
756
827
|
});
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const existing = yield* storage.getRecord(collectionName, id);
|
|
777
|
-
if (!existing || existing._d) {
|
|
778
|
-
return yield* new NotFoundError({
|
|
828
|
+
return id;
|
|
829
|
+
})
|
|
830
|
+
),
|
|
831
|
+
update: (id, data) => withLog(
|
|
832
|
+
Effect6.gen(function* () {
|
|
833
|
+
const existing = yield* storage.getRecord(collectionName, id);
|
|
834
|
+
if (!existing || existing._d) {
|
|
835
|
+
return yield* new NotFoundError({
|
|
836
|
+
collection: collectionName,
|
|
837
|
+
id
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
yield* partialValidator(data);
|
|
841
|
+
const { _d, _u, _a, _e, ...existingFields } = existing;
|
|
842
|
+
const merged = { ...existingFields, ...data, id };
|
|
843
|
+
yield* validator(merged);
|
|
844
|
+
const diff = deepDiff(existingFields, merged);
|
|
845
|
+
const event = {
|
|
846
|
+
id: makeEventId(),
|
|
779
847
|
collection: collectionName,
|
|
780
|
-
id
|
|
848
|
+
recordId: id,
|
|
849
|
+
kind: "u",
|
|
850
|
+
data: diff ?? { id },
|
|
851
|
+
createdAt: Date.now(),
|
|
852
|
+
author: localAuthor
|
|
853
|
+
};
|
|
854
|
+
yield* commitEvent(event);
|
|
855
|
+
yield* Effect6.logDebug("Record updated", {
|
|
856
|
+
collection: collectionName,
|
|
857
|
+
recordId: id,
|
|
858
|
+
data: diff
|
|
781
859
|
});
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
860
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
861
|
+
})
|
|
862
|
+
),
|
|
863
|
+
delete: (id) => withLog(
|
|
864
|
+
Effect6.gen(function* () {
|
|
865
|
+
const existing = yield* storage.getRecord(collectionName, id);
|
|
866
|
+
if (!existing || existing._d) {
|
|
867
|
+
return yield* new NotFoundError({
|
|
868
|
+
collection: collectionName,
|
|
869
|
+
id
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const event = {
|
|
873
|
+
id: makeEventId(),
|
|
874
|
+
collection: collectionName,
|
|
875
|
+
recordId: id,
|
|
876
|
+
kind: "d",
|
|
877
|
+
data: null,
|
|
878
|
+
createdAt: Date.now(),
|
|
879
|
+
author: localAuthor
|
|
880
|
+
};
|
|
881
|
+
yield* commitEvent(event);
|
|
882
|
+
yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
883
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
884
|
+
})
|
|
885
|
+
),
|
|
795
886
|
undo: (id) => Effect6.gen(function* () {
|
|
796
887
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
797
888
|
if (!existing) {
|
|
@@ -832,15 +923,29 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
832
923
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
833
924
|
}),
|
|
834
925
|
count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
835
|
-
watch: () => watchCollection(watchCtx, storage, collectionName,
|
|
836
|
-
where: (fieldName) => createWhereClause(
|
|
837
|
-
|
|
926
|
+
watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
|
|
927
|
+
where: (fieldName) => createWhereClause(
|
|
928
|
+
storage,
|
|
929
|
+
watchCtx,
|
|
930
|
+
collectionName,
|
|
931
|
+
def,
|
|
932
|
+
fieldName,
|
|
933
|
+
mapRecord
|
|
934
|
+
),
|
|
935
|
+
orderBy: (fieldName) => createOrderByBuilder(
|
|
936
|
+
storage,
|
|
937
|
+
watchCtx,
|
|
938
|
+
collectionName,
|
|
939
|
+
def,
|
|
940
|
+
fieldName,
|
|
941
|
+
mapRecord
|
|
942
|
+
)
|
|
838
943
|
};
|
|
839
944
|
return handle;
|
|
840
945
|
}
|
|
841
946
|
|
|
842
947
|
// src/sync/sync-service.ts
|
|
843
|
-
import { Effect as Effect8, Option as Option5, Ref as Ref3, Schedule } from "effect";
|
|
948
|
+
import { Duration, Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
844
949
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
845
950
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
846
951
|
|
|
@@ -856,8 +961,7 @@ var Mode = {
|
|
|
856
961
|
Fingerprint: 1,
|
|
857
962
|
IdList: 2
|
|
858
963
|
};
|
|
859
|
-
|
|
860
|
-
class WrappedBuffer {
|
|
964
|
+
var WrappedBuffer = class {
|
|
861
965
|
constructor(buffer) {
|
|
862
966
|
this._raw = new Uint8Array(buffer || 512);
|
|
863
967
|
this.length = buffer ? buffer.length : 0;
|
|
@@ -869,10 +973,8 @@ class WrappedBuffer {
|
|
|
869
973
|
return this._raw.byteLength;
|
|
870
974
|
}
|
|
871
975
|
extend(buf) {
|
|
872
|
-
if (buf._raw)
|
|
873
|
-
|
|
874
|
-
if (typeof buf.length !== "number")
|
|
875
|
-
throw Error("bad length");
|
|
976
|
+
if (buf._raw) buf = buf.unwrap();
|
|
977
|
+
if (typeof buf.length !== "number") throw Error("bad length");
|
|
876
978
|
const targetSize = buf.length + this.length;
|
|
877
979
|
if (this.capacity < targetSize) {
|
|
878
980
|
const oldRaw = this._raw;
|
|
@@ -895,42 +997,36 @@ class WrappedBuffer {
|
|
|
895
997
|
this.length -= n;
|
|
896
998
|
return firstSubarray;
|
|
897
999
|
}
|
|
898
|
-
}
|
|
1000
|
+
};
|
|
899
1001
|
function decodeVarInt(buf) {
|
|
900
1002
|
let res = 0;
|
|
901
|
-
while (
|
|
902
|
-
if (buf.length === 0)
|
|
903
|
-
throw Error("parse ends prematurely");
|
|
1003
|
+
while (1) {
|
|
1004
|
+
if (buf.length === 0) throw Error("parse ends prematurely");
|
|
904
1005
|
let byte = buf.shift();
|
|
905
1006
|
res = res << 7 | byte & 127;
|
|
906
|
-
if ((byte & 128) === 0)
|
|
907
|
-
break;
|
|
1007
|
+
if ((byte & 128) === 0) break;
|
|
908
1008
|
}
|
|
909
1009
|
return res;
|
|
910
1010
|
}
|
|
911
1011
|
function encodeVarInt(n) {
|
|
912
|
-
if (n === 0)
|
|
913
|
-
return new WrappedBuffer([0]);
|
|
1012
|
+
if (n === 0) return new WrappedBuffer([0]);
|
|
914
1013
|
let o = [];
|
|
915
1014
|
while (n !== 0) {
|
|
916
1015
|
o.push(n & 127);
|
|
917
1016
|
n >>>= 7;
|
|
918
1017
|
}
|
|
919
1018
|
o.reverse();
|
|
920
|
-
for (let i = 0;i < o.length - 1; i++)
|
|
921
|
-
o[i] |= 128;
|
|
1019
|
+
for (let i = 0; i < o.length - 1; i++) o[i] |= 128;
|
|
922
1020
|
return new WrappedBuffer(o);
|
|
923
1021
|
}
|
|
924
1022
|
function getByte(buf) {
|
|
925
1023
|
return getBytes(buf, 1)[0];
|
|
926
1024
|
}
|
|
927
1025
|
function getBytes(buf, n) {
|
|
928
|
-
if (buf.length < n)
|
|
929
|
-
throw Error("parse ends prematurely");
|
|
1026
|
+
if (buf.length < n) throw Error("parse ends prematurely");
|
|
930
1027
|
return buf.shiftN(n);
|
|
931
1028
|
}
|
|
932
|
-
|
|
933
|
-
class Accumulator {
|
|
1029
|
+
var Accumulator = class {
|
|
934
1030
|
constructor() {
|
|
935
1031
|
this.setToZero();
|
|
936
1032
|
if (typeof window === "undefined") {
|
|
@@ -947,15 +1043,14 @@ class Accumulator {
|
|
|
947
1043
|
let currCarry = 0, nextCarry = 0;
|
|
948
1044
|
let p = new DataView(this.buf.buffer);
|
|
949
1045
|
let po = new DataView(otherBuf.buffer);
|
|
950
|
-
for (let i = 0;i < 8; i++) {
|
|
1046
|
+
for (let i = 0; i < 8; i++) {
|
|
951
1047
|
let offset = i * 4;
|
|
952
1048
|
let orig = p.getUint32(offset, true);
|
|
953
1049
|
let otherV = po.getUint32(offset, true);
|
|
954
1050
|
let next = orig;
|
|
955
1051
|
next += currCarry;
|
|
956
1052
|
next += otherV;
|
|
957
|
-
if (next > 4294967295)
|
|
958
|
-
nextCarry = 1;
|
|
1053
|
+
if (next > 4294967295) nextCarry = 1;
|
|
959
1054
|
p.setUint32(offset, next & 4294967295, true);
|
|
960
1055
|
currCarry = nextCarry;
|
|
961
1056
|
nextCarry = 0;
|
|
@@ -963,7 +1058,7 @@ class Accumulator {
|
|
|
963
1058
|
}
|
|
964
1059
|
negate() {
|
|
965
1060
|
let p = new DataView(this.buf.buffer);
|
|
966
|
-
for (let i = 0;i < 8; i++) {
|
|
1061
|
+
for (let i = 0; i < 8; i++) {
|
|
967
1062
|
let offset = i * 4;
|
|
968
1063
|
p.setUint32(offset, ~p.getUint32(offset, true));
|
|
969
1064
|
}
|
|
@@ -972,33 +1067,29 @@ class Accumulator {
|
|
|
972
1067
|
this.add(one);
|
|
973
1068
|
}
|
|
974
1069
|
async getFingerprint(n) {
|
|
975
|
-
let input = new WrappedBuffer;
|
|
1070
|
+
let input = new WrappedBuffer();
|
|
976
1071
|
input.extend(this.buf);
|
|
977
1072
|
input.extend(encodeVarInt(n));
|
|
978
1073
|
let hash = await this.sha256(input.unwrap());
|
|
979
1074
|
return hash.subarray(0, FINGERPRINT_SIZE);
|
|
980
1075
|
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
class NegentropyStorageVector {
|
|
1076
|
+
};
|
|
1077
|
+
var NegentropyStorageVector = class {
|
|
984
1078
|
constructor() {
|
|
985
1079
|
this.items = [];
|
|
986
1080
|
this.sealed = false;
|
|
987
1081
|
}
|
|
988
1082
|
insert(timestamp, id) {
|
|
989
|
-
if (this.sealed)
|
|
990
|
-
throw Error("already sealed");
|
|
1083
|
+
if (this.sealed) throw Error("already sealed");
|
|
991
1084
|
id = loadInputBuffer(id);
|
|
992
|
-
if (id.byteLength !== ID_SIZE)
|
|
993
|
-
throw Error("bad id size for added item");
|
|
1085
|
+
if (id.byteLength !== ID_SIZE) throw Error("bad id size for added item");
|
|
994
1086
|
this.items.push({ timestamp, id });
|
|
995
1087
|
}
|
|
996
1088
|
seal() {
|
|
997
|
-
if (this.sealed)
|
|
998
|
-
throw Error("already sealed");
|
|
1089
|
+
if (this.sealed) throw Error("already sealed");
|
|
999
1090
|
this.sealed = true;
|
|
1000
1091
|
this.items.sort(itemCompare);
|
|
1001
|
-
for (let i = 1;i < this.items.length; i++) {
|
|
1092
|
+
for (let i = 1; i < this.items.length; i++) {
|
|
1002
1093
|
if (itemCompare(this.items[i - 1], this.items[i]) === 0)
|
|
1003
1094
|
throw Error("duplicate item inserted");
|
|
1004
1095
|
}
|
|
@@ -1012,16 +1103,14 @@ class NegentropyStorageVector {
|
|
|
1012
1103
|
}
|
|
1013
1104
|
getItem(i) {
|
|
1014
1105
|
this._checkSealed();
|
|
1015
|
-
if (i >= this.items.length)
|
|
1016
|
-
throw Error("out of range");
|
|
1106
|
+
if (i >= this.items.length) throw Error("out of range");
|
|
1017
1107
|
return this.items[i];
|
|
1018
1108
|
}
|
|
1019
1109
|
iterate(begin, end, cb) {
|
|
1020
1110
|
this._checkSealed();
|
|
1021
1111
|
this._checkBounds(begin, end);
|
|
1022
|
-
for (let i = begin;i < end; ++i) {
|
|
1023
|
-
if (!cb(this.items[i], i))
|
|
1024
|
-
break;
|
|
1112
|
+
for (let i = begin; i < end; ++i) {
|
|
1113
|
+
if (!cb(this.items[i], i)) break;
|
|
1025
1114
|
}
|
|
1026
1115
|
}
|
|
1027
1116
|
findLowerBound(begin, end, bound) {
|
|
@@ -1030,7 +1119,7 @@ class NegentropyStorageVector {
|
|
|
1030
1119
|
return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0);
|
|
1031
1120
|
}
|
|
1032
1121
|
async fingerprint(begin, end) {
|
|
1033
|
-
let out = new Accumulator;
|
|
1122
|
+
let out = new Accumulator();
|
|
1034
1123
|
out.setToZero();
|
|
1035
1124
|
this.iterate(begin, end, (item, i) => {
|
|
1036
1125
|
out.add(item.id);
|
|
@@ -1039,12 +1128,10 @@ class NegentropyStorageVector {
|
|
|
1039
1128
|
return await out.getFingerprint(end - begin);
|
|
1040
1129
|
}
|
|
1041
1130
|
_checkSealed() {
|
|
1042
|
-
if (!this.sealed)
|
|
1043
|
-
throw Error("not sealed");
|
|
1131
|
+
if (!this.sealed) throw Error("not sealed");
|
|
1044
1132
|
}
|
|
1045
1133
|
_checkBounds(begin, end) {
|
|
1046
|
-
if (begin > end || end > this.items.length)
|
|
1047
|
-
throw Error("bad range");
|
|
1134
|
+
if (begin > end || end > this.items.length) throw Error("bad range");
|
|
1048
1135
|
}
|
|
1049
1136
|
_binarySearch(arr, first, last, cmp) {
|
|
1050
1137
|
let count = last - first;
|
|
@@ -1061,12 +1148,10 @@ class NegentropyStorageVector {
|
|
|
1061
1148
|
}
|
|
1062
1149
|
return first;
|
|
1063
1150
|
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
class Negentropy {
|
|
1151
|
+
};
|
|
1152
|
+
var Negentropy = class {
|
|
1067
1153
|
constructor(storage, frameSizeLimit = 0) {
|
|
1068
|
-
if (frameSizeLimit !== 0 && frameSizeLimit < 4096)
|
|
1069
|
-
throw Error("frameSizeLimit too small");
|
|
1154
|
+
if (frameSizeLimit !== 0 && frameSizeLimit < 4096) throw Error("frameSizeLimit too small");
|
|
1070
1155
|
this.storage = storage;
|
|
1071
1156
|
this.frameSizeLimit = frameSizeLimit;
|
|
1072
1157
|
this.lastTimestampIn = 0;
|
|
@@ -1076,10 +1161,9 @@ class Negentropy {
|
|
|
1076
1161
|
return { timestamp, id: id ? id : new Uint8Array(0) };
|
|
1077
1162
|
}
|
|
1078
1163
|
async initiate() {
|
|
1079
|
-
if (this.isInitiator)
|
|
1080
|
-
throw Error("already initiated");
|
|
1164
|
+
if (this.isInitiator) throw Error("already initiated");
|
|
1081
1165
|
this.isInitiator = true;
|
|
1082
|
-
let output = new WrappedBuffer;
|
|
1166
|
+
let output = new WrappedBuffer();
|
|
1083
1167
|
output.extend([PROTOCOL_VERSION]);
|
|
1084
1168
|
await this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output);
|
|
1085
1169
|
return this._renderOutput(output);
|
|
@@ -1091,23 +1175,24 @@ class Negentropy {
|
|
|
1091
1175
|
let haveIds = [], needIds = [];
|
|
1092
1176
|
query = new WrappedBuffer(loadInputBuffer(query));
|
|
1093
1177
|
this.lastTimestampIn = this.lastTimestampOut = 0;
|
|
1094
|
-
let fullOutput = new WrappedBuffer;
|
|
1178
|
+
let fullOutput = new WrappedBuffer();
|
|
1095
1179
|
fullOutput.extend([PROTOCOL_VERSION]);
|
|
1096
1180
|
let protocolVersion = getByte(query);
|
|
1097
1181
|
if (protocolVersion < 96 || protocolVersion > 111)
|
|
1098
1182
|
throw Error("invalid negentropy protocol version byte");
|
|
1099
1183
|
if (protocolVersion !== PROTOCOL_VERSION) {
|
|
1100
1184
|
if (this.isInitiator)
|
|
1101
|
-
throw Error(
|
|
1102
|
-
|
|
1103
|
-
|
|
1185
|
+
throw Error(
|
|
1186
|
+
"unsupported negentropy protocol version requested: " + (protocolVersion - 96)
|
|
1187
|
+
);
|
|
1188
|
+
else return [this._renderOutput(fullOutput), haveIds, needIds];
|
|
1104
1189
|
}
|
|
1105
1190
|
let storageSize = this.storage.size();
|
|
1106
1191
|
let prevBound = this._bound(0);
|
|
1107
1192
|
let prevIndex = 0;
|
|
1108
1193
|
let skip = false;
|
|
1109
1194
|
while (query.length !== 0) {
|
|
1110
|
-
let o = new WrappedBuffer;
|
|
1195
|
+
let o = new WrappedBuffer();
|
|
1111
1196
|
let doSkip = () => {
|
|
1112
1197
|
if (skip) {
|
|
1113
1198
|
skip = false;
|
|
@@ -1133,10 +1218,9 @@ class Negentropy {
|
|
|
1133
1218
|
} else if (mode === Mode.IdList) {
|
|
1134
1219
|
let numIds = decodeVarInt(query);
|
|
1135
1220
|
let theirElems = {};
|
|
1136
|
-
for (let i = 0;i < numIds; i++) {
|
|
1221
|
+
for (let i = 0; i < numIds; i++) {
|
|
1137
1222
|
let e = getBytes(query, ID_SIZE);
|
|
1138
|
-
if (this.isInitiator)
|
|
1139
|
-
theirElems[e] = e;
|
|
1223
|
+
if (this.isInitiator) theirElems[e] = e;
|
|
1140
1224
|
}
|
|
1141
1225
|
if (this.isInitiator) {
|
|
1142
1226
|
skip = true;
|
|
@@ -1155,7 +1239,7 @@ class Negentropy {
|
|
|
1155
1239
|
}
|
|
1156
1240
|
} else {
|
|
1157
1241
|
doSkip();
|
|
1158
|
-
let responseIds = new WrappedBuffer;
|
|
1242
|
+
let responseIds = new WrappedBuffer();
|
|
1159
1243
|
let numResponseIds = 0;
|
|
1160
1244
|
let endBound = currBound;
|
|
1161
1245
|
this.storage.iterate(lower, upper, (item, index) => {
|
|
@@ -1173,7 +1257,7 @@ class Negentropy {
|
|
|
1173
1257
|
o.extend(encodeVarInt(numResponseIds));
|
|
1174
1258
|
o.extend(responseIds);
|
|
1175
1259
|
fullOutput.extend(o);
|
|
1176
|
-
o = new WrappedBuffer;
|
|
1260
|
+
o = new WrappedBuffer();
|
|
1177
1261
|
}
|
|
1178
1262
|
} else {
|
|
1179
1263
|
throw Error("unexpected mode");
|
|
@@ -1211,7 +1295,7 @@ class Negentropy {
|
|
|
1211
1295
|
let itemsPerBucket = Math.floor(numElems / buckets);
|
|
1212
1296
|
let bucketsWithExtra = numElems % buckets;
|
|
1213
1297
|
let curr = lower;
|
|
1214
|
-
for (let i = 0;i < buckets; i++) {
|
|
1298
|
+
for (let i = 0; i < buckets; i++) {
|
|
1215
1299
|
let bucketSize = itemsPerBucket + (i < bucketsWithExtra ? 1 : 0);
|
|
1216
1300
|
let ourFingerprint = await this.storage.fingerprint(curr, curr + bucketSize);
|
|
1217
1301
|
curr += bucketSize;
|
|
@@ -1221,10 +1305,8 @@ class Negentropy {
|
|
|
1221
1305
|
} else {
|
|
1222
1306
|
let prevItem, currItem;
|
|
1223
1307
|
this.storage.iterate(curr - 1, curr + 1, (item, index) => {
|
|
1224
|
-
if (index === curr - 1)
|
|
1225
|
-
|
|
1226
|
-
else
|
|
1227
|
-
currItem = item;
|
|
1308
|
+
if (index === curr - 1) prevItem = item;
|
|
1309
|
+
else currItem = item;
|
|
1228
1310
|
return true;
|
|
1229
1311
|
});
|
|
1230
1312
|
nextBound = this.getMinimalBound(prevItem, currItem);
|
|
@@ -1237,13 +1319,13 @@ class Negentropy {
|
|
|
1237
1319
|
}
|
|
1238
1320
|
_renderOutput(o) {
|
|
1239
1321
|
o = o.unwrap();
|
|
1240
|
-
if (!this.wantUint8ArrayOutput)
|
|
1241
|
-
o = uint8ArrayToHex(o);
|
|
1322
|
+
if (!this.wantUint8ArrayOutput) o = uint8ArrayToHex(o);
|
|
1242
1323
|
return o;
|
|
1243
1324
|
}
|
|
1244
1325
|
exceededFrameSizeLimit(n) {
|
|
1245
1326
|
return this.frameSizeLimit && n > this.frameSizeLimit - 200;
|
|
1246
1327
|
}
|
|
1328
|
+
// Decoding
|
|
1247
1329
|
decodeTimestampIn(encoded) {
|
|
1248
1330
|
let timestamp = decodeVarInt(encoded);
|
|
1249
1331
|
timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1;
|
|
@@ -1258,11 +1340,11 @@ class Negentropy {
|
|
|
1258
1340
|
decodeBound(encoded) {
|
|
1259
1341
|
let timestamp = this.decodeTimestampIn(encoded);
|
|
1260
1342
|
let len = decodeVarInt(encoded);
|
|
1261
|
-
if (len > ID_SIZE)
|
|
1262
|
-
throw Error("bound key too long");
|
|
1343
|
+
if (len > ID_SIZE) throw Error("bound key too long");
|
|
1263
1344
|
let id = getBytes(encoded, len);
|
|
1264
1345
|
return { timestamp, id };
|
|
1265
1346
|
}
|
|
1347
|
+
// Encoding
|
|
1266
1348
|
encodeTimestampOut(timestamp) {
|
|
1267
1349
|
if (timestamp === Number.MAX_VALUE) {
|
|
1268
1350
|
this.lastTimestampOut = Number.MAX_VALUE;
|
|
@@ -1274,7 +1356,7 @@ class Negentropy {
|
|
|
1274
1356
|
return encodeVarInt(timestamp + 1);
|
|
1275
1357
|
}
|
|
1276
1358
|
encodeBound(key) {
|
|
1277
|
-
let output = new WrappedBuffer;
|
|
1359
|
+
let output = new WrappedBuffer();
|
|
1278
1360
|
output.extend(this.encodeTimestampOut(key.timestamp));
|
|
1279
1361
|
output.extend(encodeVarInt(key.id.length));
|
|
1280
1362
|
output.extend(key.id);
|
|
@@ -1287,30 +1369,24 @@ class Negentropy {
|
|
|
1287
1369
|
let sharedPrefixBytes = 0;
|
|
1288
1370
|
let currKey = curr.id;
|
|
1289
1371
|
let prevKey = prev.id;
|
|
1290
|
-
for (let i = 0;i < ID_SIZE; i++) {
|
|
1291
|
-
if (currKey[i] !== prevKey[i])
|
|
1292
|
-
break;
|
|
1372
|
+
for (let i = 0; i < ID_SIZE; i++) {
|
|
1373
|
+
if (currKey[i] !== prevKey[i]) break;
|
|
1293
1374
|
sharedPrefixBytes++;
|
|
1294
1375
|
}
|
|
1295
1376
|
return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1));
|
|
1296
1377
|
}
|
|
1297
1378
|
}
|
|
1298
|
-
}
|
|
1379
|
+
};
|
|
1299
1380
|
function loadInputBuffer(inp) {
|
|
1300
|
-
if (typeof inp === "string")
|
|
1301
|
-
|
|
1302
|
-
else if (__proto__ !== Uint8Array.prototype)
|
|
1303
|
-
inp = new Uint8Array(inp);
|
|
1381
|
+
if (typeof inp === "string") inp = hexToUint8Array(inp);
|
|
1382
|
+
else if (__proto__ !== Uint8Array.prototype) inp = new Uint8Array(inp);
|
|
1304
1383
|
return inp;
|
|
1305
1384
|
}
|
|
1306
1385
|
function hexToUint8Array(h) {
|
|
1307
|
-
if (h.startsWith("0x"))
|
|
1308
|
-
|
|
1309
|
-
if (h.length % 2 === 1)
|
|
1310
|
-
throw Error("odd length of hex string");
|
|
1386
|
+
if (h.startsWith("0x")) h = h.substr(2);
|
|
1387
|
+
if (h.length % 2 === 1) throw Error("odd length of hex string");
|
|
1311
1388
|
let arr = new Uint8Array(h.length / 2);
|
|
1312
|
-
for (let i = 0;i < arr.length; i++)
|
|
1313
|
-
arr[i] = parseInt(h.substr(i * 2, 2), 16);
|
|
1389
|
+
for (let i = 0; i < arr.length; i++) arr[i] = parseInt(h.substr(i * 2, 2), 16);
|
|
1314
1390
|
return arr;
|
|
1315
1391
|
}
|
|
1316
1392
|
var uint8ArrayToHexLookupTable = new Array(256);
|
|
@@ -1333,28 +1409,24 @@ var uint8ArrayToHexLookupTable = new Array(256);
|
|
|
1333
1409
|
"e",
|
|
1334
1410
|
"f"
|
|
1335
1411
|
];
|
|
1336
|
-
for (let i = 0;i < 256; i++) {
|
|
1412
|
+
for (let i = 0; i < 256; i++) {
|
|
1337
1413
|
uint8ArrayToHexLookupTable[i] = hexAlphabet[i >>> 4 & 15] + hexAlphabet[i & 15];
|
|
1338
1414
|
}
|
|
1339
1415
|
}
|
|
1340
1416
|
function uint8ArrayToHex(arr) {
|
|
1341
1417
|
let out = "";
|
|
1342
|
-
for (let i = 0, edx = arr.length;i < edx; i++) {
|
|
1418
|
+
for (let i = 0, edx = arr.length; i < edx; i++) {
|
|
1343
1419
|
out += uint8ArrayToHexLookupTable[arr[i]];
|
|
1344
1420
|
}
|
|
1345
1421
|
return out;
|
|
1346
1422
|
}
|
|
1347
1423
|
function compareUint8Array(a, b) {
|
|
1348
|
-
for (let i = 0;i < a.byteLength; i++) {
|
|
1349
|
-
if (a[i] < b[i])
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
if (a.byteLength > b.byteLength)
|
|
1355
|
-
return 1;
|
|
1356
|
-
if (a.byteLength < b.byteLength)
|
|
1357
|
-
return -1;
|
|
1424
|
+
for (let i = 0; i < a.byteLength; i++) {
|
|
1425
|
+
if (a[i] < b[i]) return -1;
|
|
1426
|
+
if (a[i] > b[i]) return 1;
|
|
1427
|
+
}
|
|
1428
|
+
if (a.byteLength > b.byteLength) return 1;
|
|
1429
|
+
if (a.byteLength < b.byteLength) return -1;
|
|
1358
1430
|
return 0;
|
|
1359
1431
|
}
|
|
1360
1432
|
function itemCompare(a, b) {
|
|
@@ -1370,7 +1442,7 @@ import { GiftWrap } from "nostr-tools/kinds";
|
|
|
1370
1442
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1371
1443
|
return Effect7.gen(function* () {
|
|
1372
1444
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1373
|
-
const storageVector = new NegentropyStorageVector;
|
|
1445
|
+
const storageVector = new NegentropyStorageVector();
|
|
1374
1446
|
for (const gw of allGiftWraps) {
|
|
1375
1447
|
storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
|
|
1376
1448
|
}
|
|
@@ -1394,8 +1466,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1394
1466
|
let currentMsg = initialMsg;
|
|
1395
1467
|
while (currentMsg !== null) {
|
|
1396
1468
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1397
|
-
if (response.msgHex === null)
|
|
1398
|
-
break;
|
|
1469
|
+
if (response.msgHex === null) break;
|
|
1399
1470
|
const reconcileResult = yield* Effect7.try({
|
|
1400
1471
|
try: () => neg.reconcile(response.msgHex),
|
|
1401
1472
|
catch: (e) => new SyncError({
|
|
@@ -1405,14 +1476,17 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1405
1476
|
})
|
|
1406
1477
|
});
|
|
1407
1478
|
const [nextMsg, haveIds, needIds] = reconcileResult;
|
|
1408
|
-
for (const id of haveIds)
|
|
1409
|
-
|
|
1410
|
-
for (const id of needIds)
|
|
1411
|
-
allNeedIds.push(id);
|
|
1479
|
+
for (const id of haveIds) allHaveIds.push(id);
|
|
1480
|
+
for (const id of needIds) allNeedIds.push(id);
|
|
1412
1481
|
currentMsg = nextMsg;
|
|
1413
1482
|
}
|
|
1483
|
+
yield* Effect7.logDebug("Negentropy reconciliation complete", {
|
|
1484
|
+
relay: relayUrl,
|
|
1485
|
+
have: allHaveIds.length,
|
|
1486
|
+
need: allNeedIds.length
|
|
1487
|
+
});
|
|
1414
1488
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1415
|
-
});
|
|
1489
|
+
}).pipe(Effect7.withLogSpan("tablinum.negentropy"));
|
|
1416
1490
|
}
|
|
1417
1491
|
|
|
1418
1492
|
// src/db/key-rotation.ts
|
|
@@ -1451,12 +1525,11 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1451
1525
|
kind: 1,
|
|
1452
1526
|
content: JSON.stringify(rotationData),
|
|
1453
1527
|
tags: [["d", `_system:rotation:${epochId}`]],
|
|
1454
|
-
created_at: Math.floor(Date.now() /
|
|
1528
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1455
1529
|
};
|
|
1456
1530
|
const wrappedEvents = [];
|
|
1457
1531
|
for (const memberPubkey of remainingMemberPubkeys) {
|
|
1458
|
-
if (memberPubkey === senderPublicKey)
|
|
1459
|
-
continue;
|
|
1532
|
+
if (memberPubkey === senderPublicKey) continue;
|
|
1460
1533
|
const wrapped = wrapEvent(rumor, senderPrivateKey, memberPubkey);
|
|
1461
1534
|
wrappedEvents.push(wrapped);
|
|
1462
1535
|
}
|
|
@@ -1469,7 +1542,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1469
1542
|
kind: 1,
|
|
1470
1543
|
content: JSON.stringify(removalData),
|
|
1471
1544
|
tags: [["d", `_system:removed:${epochId}`]],
|
|
1472
|
-
created_at: Math.floor(Date.now() /
|
|
1545
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1473
1546
|
};
|
|
1474
1547
|
const removalNotices = [];
|
|
1475
1548
|
for (const removedPubkey of removedMemberPubkeys) {
|
|
@@ -1479,8 +1552,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1479
1552
|
return { epoch, wrappedEvents, removalNotices };
|
|
1480
1553
|
}
|
|
1481
1554
|
function parseRotationEvent(content, dTag) {
|
|
1482
|
-
if (!dTag.startsWith("_system:rotation:"))
|
|
1483
|
-
return Option4.none();
|
|
1555
|
+
if (!dTag.startsWith("_system:rotation:")) return Option4.none();
|
|
1484
1556
|
try {
|
|
1485
1557
|
return Option4.some(decodeRotationData(content));
|
|
1486
1558
|
} catch {
|
|
@@ -1488,8 +1560,7 @@ function parseRotationEvent(content, dTag) {
|
|
|
1488
1560
|
}
|
|
1489
1561
|
}
|
|
1490
1562
|
function parseRemovalNotice(content, dTag) {
|
|
1491
|
-
if (!dTag.startsWith("_system:removed:"))
|
|
1492
|
-
return Option4.none();
|
|
1563
|
+
if (!dTag.startsWith("_system:removed:")) return Option4.none();
|
|
1493
1564
|
try {
|
|
1494
1565
|
return Option4.some(decodeRemovalNotice(content));
|
|
1495
1566
|
} catch {
|
|
@@ -1498,7 +1569,8 @@ function parseRemovalNotice(content, dTag) {
|
|
|
1498
1569
|
}
|
|
1499
1570
|
|
|
1500
1571
|
// src/sync/sync-service.ts
|
|
1501
|
-
function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
|
|
1572
|
+
function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, logLevel, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
|
|
1573
|
+
const logLayer = Layer.succeed(References2.MinimumLogLevel, logLevel);
|
|
1502
1574
|
const getSubscriptionPubKeys = () => {
|
|
1503
1575
|
return getAllPublicKeys(epochStore);
|
|
1504
1576
|
};
|
|
@@ -1508,64 +1580,79 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1508
1580
|
kind: "create"
|
|
1509
1581
|
});
|
|
1510
1582
|
const forkHandled = (effect) => {
|
|
1511
|
-
Effect8.runFork(
|
|
1583
|
+
Effect8.runFork(
|
|
1584
|
+
effect.pipe(
|
|
1585
|
+
Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))),
|
|
1586
|
+
Effect8.ignore,
|
|
1587
|
+
Effect8.provide(logLayer),
|
|
1588
|
+
Effect8.forkIn(scope)
|
|
1589
|
+
)
|
|
1590
|
+
);
|
|
1512
1591
|
};
|
|
1513
1592
|
let autoFlushActive = false;
|
|
1514
1593
|
const autoFlushEffect = Effect8.gen(function* () {
|
|
1515
1594
|
const size = yield* publishQueue.size();
|
|
1516
|
-
if (size === 0)
|
|
1517
|
-
return;
|
|
1595
|
+
if (size === 0) return;
|
|
1518
1596
|
yield* syncStatus.set("syncing");
|
|
1519
1597
|
yield* publishQueue.flush(relayUrls);
|
|
1520
1598
|
const remaining = yield* publishQueue.size();
|
|
1521
|
-
if (remaining > 0)
|
|
1522
|
-
|
|
1523
|
-
|
|
1599
|
+
if (remaining > 0) yield* Effect8.fail("pending");
|
|
1600
|
+
}).pipe(
|
|
1601
|
+
Effect8.ensuring(syncStatus.set("idle")),
|
|
1602
|
+
Effect8.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
|
|
1603
|
+
Effect8.ignore
|
|
1604
|
+
);
|
|
1524
1605
|
const scheduleAutoFlush = () => {
|
|
1525
|
-
if (autoFlushActive)
|
|
1526
|
-
return;
|
|
1606
|
+
if (autoFlushActive) return;
|
|
1527
1607
|
autoFlushActive = true;
|
|
1528
|
-
forkHandled(
|
|
1529
|
-
|
|
1530
|
-
|
|
1608
|
+
forkHandled(
|
|
1609
|
+
autoFlushEffect.pipe(
|
|
1610
|
+
Effect8.ensuring(
|
|
1611
|
+
Effect8.sync(() => {
|
|
1612
|
+
autoFlushActive = false;
|
|
1613
|
+
})
|
|
1614
|
+
)
|
|
1615
|
+
)
|
|
1616
|
+
);
|
|
1531
1617
|
};
|
|
1532
1618
|
const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
|
|
1533
1619
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1534
|
-
if (!memberRecord)
|
|
1535
|
-
return false;
|
|
1620
|
+
if (!memberRecord) return false;
|
|
1536
1621
|
return !!memberRecord.removedAt;
|
|
1537
1622
|
});
|
|
1538
1623
|
const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1539
1624
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1540
|
-
if (existing)
|
|
1541
|
-
return null;
|
|
1625
|
+
if (existing) return null;
|
|
1542
1626
|
const unwrapResult = yield* Effect8.result(giftWrapHandle.unwrap(remoteGw));
|
|
1543
1627
|
if (unwrapResult._tag === "Failure") {
|
|
1544
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1628
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1545
1629
|
return null;
|
|
1546
1630
|
}
|
|
1547
1631
|
const rumor = unwrapResult.success;
|
|
1548
1632
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1549
1633
|
if (!dTag) {
|
|
1550
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1634
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1551
1635
|
return null;
|
|
1552
1636
|
}
|
|
1553
1637
|
const colonIdx = dTag.indexOf(":");
|
|
1554
1638
|
if (colonIdx === -1) {
|
|
1555
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1639
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1556
1640
|
return null;
|
|
1557
1641
|
}
|
|
1558
1642
|
const collectionName = dTag.substring(0, colonIdx);
|
|
1559
1643
|
const recordId = dTag.substring(colonIdx + 1);
|
|
1560
1644
|
const retention = knownCollections.get(collectionName);
|
|
1561
|
-
if (retention ===
|
|
1562
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1645
|
+
if (retention === void 0) {
|
|
1646
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1563
1647
|
return null;
|
|
1564
1648
|
}
|
|
1565
1649
|
if (rumor.pubkey) {
|
|
1566
1650
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1567
1651
|
if (reject) {
|
|
1568
|
-
yield*
|
|
1652
|
+
yield* Effect8.logWarning("Rejected write from removed member", {
|
|
1653
|
+
author: rumor.pubkey.slice(0, 12)
|
|
1654
|
+
});
|
|
1655
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1569
1656
|
return null;
|
|
1570
1657
|
}
|
|
1571
1658
|
}
|
|
@@ -1573,14 +1660,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1573
1660
|
let kind = "u";
|
|
1574
1661
|
const parsed = yield* Effect8.try({
|
|
1575
1662
|
try: () => JSON.parse(rumor.content),
|
|
1576
|
-
catch: () =>
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
return;
|
|
1581
|
-
}));
|
|
1582
|
-
if (parsed === undefined) {
|
|
1583
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1663
|
+
catch: () => void 0
|
|
1664
|
+
}).pipe(Effect8.orElseSucceed(() => void 0));
|
|
1665
|
+
if (parsed === void 0) {
|
|
1666
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1584
1667
|
return null;
|
|
1585
1668
|
}
|
|
1586
1669
|
if (parsed === null || parsed._deleted) {
|
|
@@ -1588,18 +1671,19 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1588
1671
|
} else {
|
|
1589
1672
|
data = parsed;
|
|
1590
1673
|
}
|
|
1591
|
-
const author = rumor.pubkey ||
|
|
1674
|
+
const author = rumor.pubkey || void 0;
|
|
1592
1675
|
const event = {
|
|
1593
1676
|
id: rumor.id,
|
|
1594
1677
|
collection: collectionName,
|
|
1595
1678
|
recordId,
|
|
1596
1679
|
kind,
|
|
1597
1680
|
data,
|
|
1598
|
-
createdAt: rumor.created_at *
|
|
1681
|
+
createdAt: rumor.created_at * 1e3,
|
|
1599
1682
|
author
|
|
1600
1683
|
};
|
|
1601
1684
|
yield* storage.putGiftWrap({
|
|
1602
1685
|
id: remoteGw.id,
|
|
1686
|
+
event: remoteGw,
|
|
1603
1687
|
createdAt: remoteGw.created_at
|
|
1604
1688
|
});
|
|
1605
1689
|
yield* storage.putEvent(event);
|
|
@@ -1607,6 +1691,12 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1607
1691
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1608
1692
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1609
1693
|
}
|
|
1694
|
+
yield* Effect8.logDebug("Processed gift wrap", {
|
|
1695
|
+
collection: collectionName,
|
|
1696
|
+
recordId,
|
|
1697
|
+
kind,
|
|
1698
|
+
author: author?.slice(0, 12)
|
|
1699
|
+
});
|
|
1610
1700
|
if (author && onNewAuthor) {
|
|
1611
1701
|
onNewAuthor(author);
|
|
1612
1702
|
}
|
|
@@ -1619,32 +1709,34 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1619
1709
|
}
|
|
1620
1710
|
});
|
|
1621
1711
|
const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1622
|
-
const unwrapResult = yield* Effect8.result(
|
|
1623
|
-
try
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1712
|
+
const unwrapResult = yield* Effect8.result(
|
|
1713
|
+
Effect8.try({
|
|
1714
|
+
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1715
|
+
catch: (e) => new CryptoError({
|
|
1716
|
+
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
1717
|
+
cause: e
|
|
1718
|
+
})
|
|
1627
1719
|
})
|
|
1628
|
-
|
|
1629
|
-
if (unwrapResult._tag === "Failure")
|
|
1630
|
-
return false;
|
|
1720
|
+
);
|
|
1721
|
+
if (unwrapResult._tag === "Failure") return false;
|
|
1631
1722
|
const rumor = unwrapResult.success;
|
|
1632
1723
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1633
|
-
if (!dTag)
|
|
1634
|
-
return false;
|
|
1724
|
+
if (!dTag) return false;
|
|
1635
1725
|
const removalNoticeOpt = parseRemovalNotice(rumor.content, dTag);
|
|
1636
1726
|
if (Option5.isSome(removalNoticeOpt)) {
|
|
1637
|
-
if (onRemoved)
|
|
1638
|
-
onRemoved(removalNoticeOpt.value);
|
|
1727
|
+
if (onRemoved) onRemoved(removalNoticeOpt.value);
|
|
1639
1728
|
return true;
|
|
1640
1729
|
}
|
|
1641
1730
|
const rotationDataOpt = parseRotationEvent(rumor.content, dTag);
|
|
1642
|
-
if (Option5.isNone(rotationDataOpt))
|
|
1643
|
-
return false;
|
|
1731
|
+
if (Option5.isNone(rotationDataOpt)) return false;
|
|
1644
1732
|
const rotationData = rotationDataOpt.value;
|
|
1645
|
-
if (epochStore.epochs.has(rotationData.epochId))
|
|
1646
|
-
|
|
1647
|
-
|
|
1733
|
+
if (epochStore.epochs.has(rotationData.epochId)) return false;
|
|
1734
|
+
const epoch = createEpochKey(
|
|
1735
|
+
rotationData.epochId,
|
|
1736
|
+
rotationData.epochKey,
|
|
1737
|
+
rumor.pubkey || "",
|
|
1738
|
+
rotationData.parentEpoch
|
|
1739
|
+
);
|
|
1648
1740
|
addEpoch(epochStore, epoch);
|
|
1649
1741
|
epochStore.currentEpochId = epoch.id;
|
|
1650
1742
|
let membersChanged = false;
|
|
@@ -1664,75 +1756,158 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1664
1756
|
membersChanged = true;
|
|
1665
1757
|
}
|
|
1666
1758
|
}
|
|
1667
|
-
if (membersChanged && onMembersChanged)
|
|
1668
|
-
onMembersChanged();
|
|
1759
|
+
if (membersChanged && onMembersChanged) onMembersChanged();
|
|
1669
1760
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1670
1761
|
return true;
|
|
1671
1762
|
});
|
|
1672
|
-
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1763
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1764
|
+
relayUrls,
|
|
1765
|
+
(url) => Effect8.gen(function* () {
|
|
1766
|
+
yield* relay.subscribe(filter, url, (event) => {
|
|
1767
|
+
forkHandled(onEvent(event));
|
|
1768
|
+
}).pipe(
|
|
1769
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1770
|
+
Effect8.ignore
|
|
1771
|
+
);
|
|
1772
|
+
}),
|
|
1773
|
+
{ discard: true }
|
|
1774
|
+
);
|
|
1677
1775
|
const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1678
|
-
|
|
1776
|
+
yield* Effect8.logDebug("Syncing relay", { relay: url });
|
|
1777
|
+
const reconcileResult = yield* Effect8.result(
|
|
1778
|
+
reconcileWithRelay(storage, relay, url, Array.from(pubKeys))
|
|
1779
|
+
);
|
|
1679
1780
|
if (reconcileResult._tag === "Failure") {
|
|
1680
1781
|
onSyncError?.(reconcileResult.failure);
|
|
1681
1782
|
return;
|
|
1682
1783
|
}
|
|
1683
1784
|
const { haveIds, needIds } = reconcileResult.success;
|
|
1785
|
+
yield* Effect8.logDebug("Relay reconciliation result", {
|
|
1786
|
+
relay: url,
|
|
1787
|
+
need: needIds.length,
|
|
1788
|
+
have: haveIds.length
|
|
1789
|
+
});
|
|
1684
1790
|
if (needIds.length > 0) {
|
|
1685
|
-
const fetched = yield* relay.fetchEvents(needIds, url).pipe(
|
|
1791
|
+
const fetched = yield* relay.fetchEvents(needIds, url).pipe(
|
|
1792
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1793
|
+
Effect8.orElseSucceed(() => [])
|
|
1794
|
+
);
|
|
1686
1795
|
const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
|
|
1687
|
-
yield* Effect8.forEach(
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1796
|
+
yield* Effect8.forEach(
|
|
1797
|
+
sorted,
|
|
1798
|
+
(remoteGw) => Effect8.gen(function* () {
|
|
1799
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(
|
|
1800
|
+
Effect8.orElseSucceed(() => null)
|
|
1801
|
+
);
|
|
1802
|
+
if (collection2) changedCollections.add(collection2);
|
|
1803
|
+
}),
|
|
1804
|
+
{ discard: true }
|
|
1805
|
+
);
|
|
1692
1806
|
}
|
|
1693
1807
|
if (haveIds.length > 0) {
|
|
1694
|
-
yield* Effect8.forEach(
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1808
|
+
yield* Effect8.forEach(
|
|
1809
|
+
haveIds,
|
|
1810
|
+
(id) => Effect8.gen(function* () {
|
|
1811
|
+
const gw = yield* storage.getGiftWrap(id);
|
|
1812
|
+
if (!gw?.event) return;
|
|
1813
|
+
yield* relay.publish(gw.event, [url]).pipe(
|
|
1814
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1815
|
+
Effect8.ignore
|
|
1816
|
+
);
|
|
1817
|
+
}),
|
|
1818
|
+
{ discard: true }
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
}).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
|
|
1822
|
+
let healingActive = false;
|
|
1823
|
+
const healingEffect = Effect8.gen(function* () {
|
|
1824
|
+
if (!healingActive) return;
|
|
1825
|
+
const status = yield* syncStatus.get();
|
|
1826
|
+
if (status === "syncing") return;
|
|
1827
|
+
yield* syncStatus.set("syncing");
|
|
1828
|
+
yield* Effect8.gen(function* () {
|
|
1829
|
+
const pubKeys = getSubscriptionPubKeys();
|
|
1830
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1831
|
+
yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
|
|
1832
|
+
discard: true
|
|
1833
|
+
});
|
|
1834
|
+
if (changedCollections.size > 0) {
|
|
1835
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1836
|
+
}
|
|
1837
|
+
}).pipe(Effect8.ensuring(syncStatus.set("idle")));
|
|
1838
|
+
}).pipe(Effect8.ignore);
|
|
1702
1839
|
const handle = {
|
|
1703
1840
|
sync: () => Effect8.gen(function* () {
|
|
1841
|
+
yield* Effect8.logInfo("Sync started");
|
|
1704
1842
|
yield* syncStatus.set("syncing");
|
|
1705
1843
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1706
|
-
const changedCollections = new Set;
|
|
1844
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1707
1845
|
yield* Effect8.gen(function* () {
|
|
1708
1846
|
const pubKeys = getSubscriptionPubKeys();
|
|
1709
1847
|
yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
|
|
1710
1848
|
discard: true
|
|
1711
1849
|
});
|
|
1712
1850
|
yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
|
|
1713
|
-
}).pipe(
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1851
|
+
}).pipe(
|
|
1852
|
+
Effect8.ensuring(
|
|
1853
|
+
Effect8.gen(function* () {
|
|
1854
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1855
|
+
yield* syncStatus.set("idle");
|
|
1856
|
+
})
|
|
1857
|
+
)
|
|
1858
|
+
);
|
|
1859
|
+
yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
1860
|
+
}).pipe(Effect8.withLogSpan("tablinum.sync")),
|
|
1718
1861
|
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1719
|
-
if (!giftWrap.event)
|
|
1720
|
-
|
|
1721
|
-
|
|
1862
|
+
if (!giftWrap.event) return;
|
|
1863
|
+
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1864
|
+
Effect8.tapError(
|
|
1865
|
+
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1866
|
+
Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
1867
|
+
Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
|
|
1868
|
+
)
|
|
1869
|
+
),
|
|
1870
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1871
|
+
Effect8.ignore
|
|
1872
|
+
);
|
|
1722
1873
|
}),
|
|
1723
1874
|
startSubscription: () => Effect8.gen(function* () {
|
|
1724
1875
|
const pubKeys = getSubscriptionPubKeys();
|
|
1725
1876
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1726
1877
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1727
|
-
yield* subscribeAcrossRelays(
|
|
1878
|
+
yield* subscribeAcrossRelays(
|
|
1879
|
+
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1880
|
+
(event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
|
|
1881
|
+
);
|
|
1728
1882
|
}
|
|
1729
1883
|
}),
|
|
1730
|
-
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap)
|
|
1884
|
+
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap),
|
|
1885
|
+
startHealing: () => {
|
|
1886
|
+
if (healingActive) return;
|
|
1887
|
+
healingActive = true;
|
|
1888
|
+
forkHandled(
|
|
1889
|
+
Effect8.sleep(Duration.minutes(5)).pipe(
|
|
1890
|
+
Effect8.andThen(healingEffect),
|
|
1891
|
+
Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
1892
|
+
Effect8.ensuring(Effect8.sync(() => {
|
|
1893
|
+
healingActive = false;
|
|
1894
|
+
}))
|
|
1895
|
+
)
|
|
1896
|
+
);
|
|
1897
|
+
},
|
|
1898
|
+
stopHealing: () => {
|
|
1899
|
+
healingActive = false;
|
|
1900
|
+
}
|
|
1731
1901
|
};
|
|
1732
|
-
forkHandled(
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1902
|
+
forkHandled(
|
|
1903
|
+
publishQueue.size().pipe(
|
|
1904
|
+
Effect8.flatMap(
|
|
1905
|
+
(size) => Effect8.sync(() => {
|
|
1906
|
+
if (size > 0) scheduleAutoFlush();
|
|
1907
|
+
})
|
|
1908
|
+
)
|
|
1909
|
+
)
|
|
1910
|
+
);
|
|
1736
1911
|
return handle;
|
|
1737
1912
|
}
|
|
1738
1913
|
|
|
@@ -1788,9 +1963,14 @@ var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(Aut
|
|
|
1788
1963
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1789
1964
|
return Effect9.gen(function* () {
|
|
1790
1965
|
for (const url of relayUrls) {
|
|
1791
|
-
const result = yield* Effect9.result(
|
|
1966
|
+
const result = yield* Effect9.result(
|
|
1967
|
+
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
1968
|
+
);
|
|
1792
1969
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
1793
|
-
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1970
|
+
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1971
|
+
Effect9.map(Option6.some),
|
|
1972
|
+
Effect9.orElseSucceed(() => Option6.none())
|
|
1973
|
+
);
|
|
1794
1974
|
}
|
|
1795
1975
|
}
|
|
1796
1976
|
return Option6.none();
|
|
@@ -1799,48 +1979,47 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
|
1799
1979
|
|
|
1800
1980
|
// src/services/Identity.ts
|
|
1801
1981
|
import { ServiceMap as ServiceMap3 } from "effect";
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
}
|
|
1982
|
+
var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
|
|
1983
|
+
};
|
|
1805
1984
|
|
|
1806
1985
|
// src/services/EpochStore.ts
|
|
1807
1986
|
import { ServiceMap as ServiceMap4 } from "effect";
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1987
|
+
var EpochStore = class extends ServiceMap4.Service()(
|
|
1988
|
+
"tablinum/EpochStore"
|
|
1989
|
+
) {
|
|
1990
|
+
};
|
|
1811
1991
|
|
|
1812
1992
|
// src/services/Storage.ts
|
|
1813
1993
|
import { ServiceMap as ServiceMap5 } from "effect";
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
}
|
|
1994
|
+
var Storage = class extends ServiceMap5.Service()("tablinum/Storage") {
|
|
1995
|
+
};
|
|
1817
1996
|
|
|
1818
1997
|
// src/services/Relay.ts
|
|
1819
1998
|
import { ServiceMap as ServiceMap6 } from "effect";
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
}
|
|
1999
|
+
var Relay = class extends ServiceMap6.Service()("tablinum/Relay") {
|
|
2000
|
+
};
|
|
1823
2001
|
|
|
1824
2002
|
// src/services/GiftWrap.ts
|
|
1825
2003
|
import { ServiceMap as ServiceMap7 } from "effect";
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
}
|
|
2004
|
+
var GiftWrap3 = class extends ServiceMap7.Service()("tablinum/GiftWrap") {
|
|
2005
|
+
};
|
|
1829
2006
|
|
|
1830
2007
|
// src/services/PublishQueue.ts
|
|
1831
2008
|
import { ServiceMap as ServiceMap8 } from "effect";
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2009
|
+
var PublishQueue = class extends ServiceMap8.Service()(
|
|
2010
|
+
"tablinum/PublishQueue"
|
|
2011
|
+
) {
|
|
2012
|
+
};
|
|
1835
2013
|
|
|
1836
2014
|
// src/services/SyncStatus.ts
|
|
1837
2015
|
import { ServiceMap as ServiceMap9 } from "effect";
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2016
|
+
var SyncStatus = class extends ServiceMap9.Service()(
|
|
2017
|
+
"tablinum/SyncStatus"
|
|
2018
|
+
) {
|
|
2019
|
+
};
|
|
1841
2020
|
|
|
1842
2021
|
// src/layers/IdentityLive.ts
|
|
1843
|
-
import { Effect as Effect11, Layer } from "effect";
|
|
2022
|
+
import { Effect as Effect11, Layer as Layer2 } from "effect";
|
|
1844
2023
|
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1845
2024
|
|
|
1846
2025
|
// src/db/identity.ts
|
|
@@ -1878,47 +2057,128 @@ function createIdentity(suppliedKey) {
|
|
|
1878
2057
|
}
|
|
1879
2058
|
|
|
1880
2059
|
// src/layers/IdentityLive.ts
|
|
1881
|
-
var IdentityLive =
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2060
|
+
var IdentityLive = Layer2.effect(
|
|
2061
|
+
Identity,
|
|
2062
|
+
Effect11.gen(function* () {
|
|
2063
|
+
const config = yield* Config;
|
|
2064
|
+
const storage = yield* Storage;
|
|
2065
|
+
const idbKey = yield* storage.getMeta("identity_key");
|
|
2066
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
|
|
2067
|
+
const identity = yield* createIdentity(resolvedKey);
|
|
2068
|
+
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2069
|
+
yield* Effect11.logInfo("Identity loaded", {
|
|
2070
|
+
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2071
|
+
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2072
|
+
});
|
|
2073
|
+
return identity;
|
|
2074
|
+
})
|
|
2075
|
+
);
|
|
1890
2076
|
|
|
1891
2077
|
// src/layers/EpochStoreLive.ts
|
|
1892
|
-
import { Effect as Effect12, Layer as
|
|
2078
|
+
import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
|
|
1893
2079
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1894
2080
|
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
1895
|
-
var EpochStoreLive =
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
const
|
|
1902
|
-
if (
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2081
|
+
var EpochStoreLive = Layer3.effect(
|
|
2082
|
+
EpochStore,
|
|
2083
|
+
Effect12.gen(function* () {
|
|
2084
|
+
const config = yield* Config;
|
|
2085
|
+
const identity = yield* Identity;
|
|
2086
|
+
const storage = yield* Storage;
|
|
2087
|
+
const idbRaw = yield* storage.getMeta("epochs");
|
|
2088
|
+
if (typeof idbRaw === "string") {
|
|
2089
|
+
const idbStore = deserializeEpochStore(idbRaw);
|
|
2090
|
+
if (Option7.isSome(idbStore)) {
|
|
2091
|
+
yield* Effect12.logInfo("Epoch store loaded", {
|
|
2092
|
+
source: "storage",
|
|
2093
|
+
epochs: idbStore.value.epochs.size
|
|
2094
|
+
});
|
|
2095
|
+
return idbStore.value;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
2099
|
+
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2100
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2101
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2102
|
+
return store2;
|
|
2103
|
+
}
|
|
2104
|
+
const store = createEpochStoreFromInputs(
|
|
2105
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
|
|
2106
|
+
{ createdBy: identity.publicKey }
|
|
2107
|
+
);
|
|
2108
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2109
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2110
|
+
return store;
|
|
2111
|
+
})
|
|
2112
|
+
);
|
|
1915
2113
|
|
|
1916
2114
|
// src/layers/StorageLive.ts
|
|
1917
|
-
import { Effect as Effect14, Layer as
|
|
2115
|
+
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
1918
2116
|
|
|
1919
2117
|
// src/storage/idb.ts
|
|
1920
2118
|
import { Effect as Effect13 } from "effect";
|
|
1921
2119
|
import { openDB } from "idb";
|
|
2120
|
+
|
|
2121
|
+
// src/sync/compact-event.ts
|
|
2122
|
+
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2123
|
+
var VERSION = 1;
|
|
2124
|
+
var HEADER_SIZE = 133;
|
|
2125
|
+
function base64ToBytes(base64) {
|
|
2126
|
+
const binary = atob(base64);
|
|
2127
|
+
const bytes = new Uint8Array(binary.length);
|
|
2128
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2129
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2130
|
+
}
|
|
2131
|
+
return bytes;
|
|
2132
|
+
}
|
|
2133
|
+
function bytesToBase64(bytes) {
|
|
2134
|
+
let binary = "";
|
|
2135
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2136
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2137
|
+
}
|
|
2138
|
+
return btoa(binary);
|
|
2139
|
+
}
|
|
2140
|
+
function packEvent(event) {
|
|
2141
|
+
const pubkey = hexToBytes4(event.pubkey);
|
|
2142
|
+
const sig = hexToBytes4(event.sig);
|
|
2143
|
+
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2144
|
+
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2145
|
+
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2146
|
+
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2147
|
+
}
|
|
2148
|
+
const recipient = hexToBytes4(recipientTag[1]);
|
|
2149
|
+
const createdAtBuf = new Uint8Array(4);
|
|
2150
|
+
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2151
|
+
const content = base64ToBytes(event.content);
|
|
2152
|
+
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2153
|
+
result[0] = VERSION;
|
|
2154
|
+
result.set(pubkey, 1);
|
|
2155
|
+
result.set(sig, 33);
|
|
2156
|
+
result.set(recipient, 97);
|
|
2157
|
+
result.set(createdAtBuf, 129);
|
|
2158
|
+
result.set(content, HEADER_SIZE);
|
|
2159
|
+
return result;
|
|
2160
|
+
}
|
|
2161
|
+
function unpackEvent(id, compact) {
|
|
2162
|
+
const version = compact[0];
|
|
2163
|
+
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2164
|
+
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2165
|
+
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2166
|
+
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2167
|
+
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2168
|
+
const createdAt = dv.getUint32(0, false);
|
|
2169
|
+
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2170
|
+
return {
|
|
2171
|
+
id,
|
|
2172
|
+
pubkey,
|
|
2173
|
+
sig,
|
|
2174
|
+
created_at: createdAt,
|
|
2175
|
+
kind: 1059,
|
|
2176
|
+
tags: [["p", recipient]],
|
|
2177
|
+
content
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/storage/idb.ts
|
|
1922
2182
|
var DB_NAME = "tablinum";
|
|
1923
2183
|
function storeName(collection2) {
|
|
1924
2184
|
return `col_${collection2}`;
|
|
@@ -1949,7 +2209,7 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1949
2209
|
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
1950
2210
|
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
1951
2211
|
}
|
|
1952
|
-
const expectedStores = new Set;
|
|
2212
|
+
const expectedStores = /* @__PURE__ */ new Set();
|
|
1953
2213
|
for (const [, def] of Object.entries(schema)) {
|
|
1954
2214
|
const sn = storeName(def.name);
|
|
1955
2215
|
expectedStores.add(sn);
|
|
@@ -1963,12 +2223,10 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1963
2223
|
const existingIndices = new Set(Array.from(store.indexNames));
|
|
1964
2224
|
const wantedIndices = new Set(def.indices ?? []);
|
|
1965
2225
|
for (const idx of existingIndices) {
|
|
1966
|
-
if (!wantedIndices.has(idx))
|
|
1967
|
-
store.deleteIndex(idx);
|
|
2226
|
+
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
1968
2227
|
}
|
|
1969
2228
|
for (const idx of wantedIndices) {
|
|
1970
|
-
if (!existingIndices.has(idx))
|
|
1971
|
-
store.createIndex(idx, idx);
|
|
2229
|
+
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
1972
2230
|
}
|
|
1973
2231
|
}
|
|
1974
2232
|
}
|
|
@@ -1983,6 +2241,13 @@ function openIDBStorage(dbName, schema) {
|
|
|
1983
2241
|
return Effect13.gen(function* () {
|
|
1984
2242
|
const name = dbName ?? DB_NAME;
|
|
1985
2243
|
const schemaSig = computeSchemaSig(schema);
|
|
2244
|
+
if (typeof indexedDB === "undefined") {
|
|
2245
|
+
return yield* Effect13.fail(
|
|
2246
|
+
new StorageError({
|
|
2247
|
+
message: "IndexedDB is not available in this environment"
|
|
2248
|
+
})
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
1986
2251
|
const probeDb = yield* Effect13.tryPromise({
|
|
1987
2252
|
try: () => openDB(name),
|
|
1988
2253
|
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
@@ -1993,7 +2258,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
1993
2258
|
const storedSig = yield* Effect13.tryPromise({
|
|
1994
2259
|
try: () => probeDb.get("_meta", "schema_sig"),
|
|
1995
2260
|
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
1996
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(
|
|
2261
|
+
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
1997
2262
|
needsUpgrade = storedSig !== schemaSig;
|
|
1998
2263
|
}
|
|
1999
2264
|
probeDb.close();
|
|
@@ -2010,9 +2275,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2010
2275
|
});
|
|
2011
2276
|
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2012
2277
|
const handle = {
|
|
2013
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() =>
|
|
2014
|
-
return;
|
|
2015
|
-
})),
|
|
2278
|
+
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
2016
2279
|
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2017
2280
|
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2018
2281
|
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
@@ -2032,29 +2295,52 @@ function openIDBStorage(dbName, schema) {
|
|
|
2032
2295
|
}
|
|
2033
2296
|
return results;
|
|
2034
2297
|
}),
|
|
2035
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() =>
|
|
2036
|
-
return;
|
|
2037
|
-
})),
|
|
2298
|
+
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2038
2299
|
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2039
2300
|
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2040
|
-
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
const existing = await db.get("giftwraps", id);
|
|
2051
|
-
if (existing) {
|
|
2052
|
-
await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
|
|
2301
|
+
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2302
|
+
"getEventsByRecord",
|
|
2303
|
+
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2304
|
+
),
|
|
2305
|
+
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2306
|
+
if (gw.event) {
|
|
2307
|
+
const compact = packEvent(gw.event);
|
|
2308
|
+
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2309
|
+
} else {
|
|
2310
|
+
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2053
2311
|
}
|
|
2054
2312
|
}),
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2313
|
+
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2314
|
+
const raw = await db.get("giftwraps", id);
|
|
2315
|
+
if (!raw) return void 0;
|
|
2316
|
+
if (raw.compact) {
|
|
2317
|
+
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2318
|
+
}
|
|
2319
|
+
if (raw.event) {
|
|
2320
|
+
const compact = packEvent(raw.event);
|
|
2321
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2322
|
+
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2323
|
+
}
|
|
2324
|
+
return { id: raw.id, createdAt: raw.createdAt };
|
|
2325
|
+
}),
|
|
2326
|
+
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2327
|
+
const raws = await db.getAll("giftwraps");
|
|
2328
|
+
const results = [];
|
|
2329
|
+
for (const raw of raws) {
|
|
2330
|
+
if (raw.compact) {
|
|
2331
|
+
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2332
|
+
} else if (raw.event) {
|
|
2333
|
+
const compact = packEvent(raw.event);
|
|
2334
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2335
|
+
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2336
|
+
} else {
|
|
2337
|
+
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
return results;
|
|
2341
|
+
}),
|
|
2342
|
+
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2343
|
+
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2058
2344
|
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2059
2345
|
const existing = await db.get("events", id);
|
|
2060
2346
|
if (existing) {
|
|
@@ -2062,9 +2348,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2062
2348
|
}
|
|
2063
2349
|
}),
|
|
2064
2350
|
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2065
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() =>
|
|
2066
|
-
return;
|
|
2067
|
-
})),
|
|
2351
|
+
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2068
2352
|
close: () => Effect13.sync(() => db.close())
|
|
2069
2353
|
};
|
|
2070
2354
|
return handle;
|
|
@@ -2072,16 +2356,21 @@ function openIDBStorage(dbName, schema) {
|
|
|
2072
2356
|
}
|
|
2073
2357
|
|
|
2074
2358
|
// src/layers/StorageLive.ts
|
|
2075
|
-
var StorageLive =
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2359
|
+
var StorageLive = Layer4.effect(
|
|
2360
|
+
Storage,
|
|
2361
|
+
Effect14.gen(function* () {
|
|
2362
|
+
const config = yield* Config;
|
|
2363
|
+
const handle = yield* openIDBStorage(config.dbName, {
|
|
2364
|
+
...config.schema,
|
|
2365
|
+
_members: membersCollectionDef
|
|
2366
|
+
});
|
|
2367
|
+
yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
|
|
2368
|
+
return handle;
|
|
2369
|
+
})
|
|
2370
|
+
);
|
|
2082
2371
|
|
|
2083
2372
|
// src/layers/RelayLive.ts
|
|
2084
|
-
import { Layer as
|
|
2373
|
+
import { Layer as Layer5 } from "effect";
|
|
2085
2374
|
|
|
2086
2375
|
// src/sync/relay.ts
|
|
2087
2376
|
import { Effect as Effect15, Option as Option8, Schema as Schema7, ScopedCache, Scope as Scope3 } from "effect";
|
|
@@ -2092,32 +2381,41 @@ var NegMessageFrameSchema = Schema7.Tuple([
|
|
|
2092
2381
|
Schema7.String
|
|
2093
2382
|
]);
|
|
2094
2383
|
var NegErrorFrameSchema = Schema7.Tuple([Schema7.Literal("NEG-ERR"), Schema7.String, Schema7.String]);
|
|
2095
|
-
var decodeNegFrame = Schema7.decodeUnknownEffect(
|
|
2384
|
+
var decodeNegFrame = Schema7.decodeUnknownEffect(
|
|
2385
|
+
Schema7.fromJsonString(Schema7.Union([NegMessageFrameSchema, NegErrorFrameSchema]))
|
|
2386
|
+
);
|
|
2096
2387
|
function parseNegMessageFrame(data) {
|
|
2097
|
-
return Effect15.runSync(
|
|
2388
|
+
return Effect15.runSync(
|
|
2389
|
+
decodeNegFrame(data).pipe(
|
|
2390
|
+
Effect15.map(Option8.some),
|
|
2391
|
+
Effect15.orElseSucceed(() => Option8.none())
|
|
2392
|
+
)
|
|
2393
|
+
);
|
|
2098
2394
|
}
|
|
2099
2395
|
function createRelayHandle() {
|
|
2100
2396
|
return Effect15.gen(function* () {
|
|
2101
2397
|
const relayScope = yield* Effect15.scope;
|
|
2102
2398
|
const connections = yield* ScopedCache.make({
|
|
2103
2399
|
capacity: 64,
|
|
2104
|
-
lookup: (url) => Effect15.acquireRelease(
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2400
|
+
lookup: (url) => Effect15.acquireRelease(
|
|
2401
|
+
Effect15.tryPromise({
|
|
2402
|
+
try: () => Relay2.connect(url),
|
|
2403
|
+
catch: (e) => new RelayError({
|
|
2404
|
+
message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2405
|
+
url,
|
|
2406
|
+
cause: e
|
|
2407
|
+
})
|
|
2408
|
+
}),
|
|
2409
|
+
(relay) => Effect15.sync(() => {
|
|
2410
|
+
relay.close();
|
|
2110
2411
|
})
|
|
2111
|
-
|
|
2112
|
-
relay.close();
|
|
2113
|
-
}))
|
|
2412
|
+
)
|
|
2114
2413
|
});
|
|
2115
|
-
const connectedUrls = new Set;
|
|
2116
|
-
const statusListeners = new Set;
|
|
2414
|
+
const connectedUrls = /* @__PURE__ */ new Set();
|
|
2415
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
2117
2416
|
const notifyStatus = () => {
|
|
2118
2417
|
const status = { connectedUrls: [...connectedUrls] };
|
|
2119
|
-
for (const listener of statusListeners)
|
|
2120
|
-
listener(status);
|
|
2418
|
+
for (const listener of statusListeners) listener(status);
|
|
2121
2419
|
};
|
|
2122
2420
|
const markConnected = (url) => {
|
|
2123
2421
|
if (!connectedUrls.has(url)) {
|
|
@@ -2131,66 +2429,98 @@ function createRelayHandle() {
|
|
|
2131
2429
|
notifyStatus();
|
|
2132
2430
|
}
|
|
2133
2431
|
};
|
|
2134
|
-
const getRelay = (url) => ScopedCache.get(connections, url).pipe(
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
sub
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2432
|
+
const getRelay = (url) => ScopedCache.get(connections, url).pipe(
|
|
2433
|
+
Effect15.flatMap(
|
|
2434
|
+
(relay) => relay.connected === false ? ScopedCache.invalidate(connections, url).pipe(
|
|
2435
|
+
Effect15.andThen(ScopedCache.get(connections, url))
|
|
2436
|
+
) : Effect15.succeed(relay)
|
|
2437
|
+
)
|
|
2438
|
+
);
|
|
2439
|
+
const withRelay = (url, run) => getRelay(url).pipe(
|
|
2440
|
+
Effect15.tap(() => Effect15.sync(() => markConnected(url))),
|
|
2441
|
+
Effect15.flatMap((relay) => run(relay)),
|
|
2442
|
+
Effect15.tapError(
|
|
2443
|
+
() => ScopedCache.invalidate(connections, url).pipe(
|
|
2444
|
+
Effect15.tap(() => Effect15.sync(() => markDisconnected(url)))
|
|
2445
|
+
)
|
|
2446
|
+
)
|
|
2447
|
+
);
|
|
2448
|
+
const collectEvents = (url, filters) => withRelay(
|
|
2449
|
+
url,
|
|
2450
|
+
(relay) => Effect15.callback((resume) => {
|
|
2451
|
+
const events = [];
|
|
2452
|
+
let settled = false;
|
|
2453
|
+
let timer;
|
|
2454
|
+
let sub;
|
|
2455
|
+
const cleanup = () => {
|
|
2456
|
+
settled = true;
|
|
2457
|
+
if (timer !== void 0) {
|
|
2458
|
+
clearTimeout(timer);
|
|
2459
|
+
timer = void 0;
|
|
2460
|
+
}
|
|
2461
|
+
sub?.close();
|
|
2462
|
+
sub = void 0;
|
|
2463
|
+
};
|
|
2464
|
+
const fail = (e) => resume(
|
|
2465
|
+
Effect15.fail(
|
|
2466
|
+
new RelayError({
|
|
2467
|
+
message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2468
|
+
url,
|
|
2469
|
+
cause: e
|
|
2470
|
+
})
|
|
2471
|
+
)
|
|
2472
|
+
);
|
|
2473
|
+
try {
|
|
2474
|
+
sub = relay.subscribe([...filters], {
|
|
2475
|
+
onevent(evt) {
|
|
2476
|
+
if (!settled) {
|
|
2477
|
+
events.push(evt);
|
|
2478
|
+
}
|
|
2479
|
+
},
|
|
2480
|
+
oneose() {
|
|
2481
|
+
if (settled) return;
|
|
2482
|
+
cleanup();
|
|
2483
|
+
resume(Effect15.succeed(events));
|
|
2160
2484
|
}
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
if (settled)
|
|
2164
|
-
return;
|
|
2485
|
+
});
|
|
2486
|
+
timer = setTimeout(() => {
|
|
2487
|
+
if (settled) return;
|
|
2165
2488
|
cleanup();
|
|
2166
2489
|
resume(Effect15.succeed(events));
|
|
2167
|
-
}
|
|
2168
|
-
})
|
|
2169
|
-
timer = setTimeout(() => {
|
|
2170
|
-
if (settled)
|
|
2171
|
-
return;
|
|
2490
|
+
}, 1e4);
|
|
2491
|
+
} catch (e) {
|
|
2172
2492
|
cleanup();
|
|
2173
|
-
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
}
|
|
2179
|
-
return Effect15.sync(cleanup);
|
|
2180
|
-
}));
|
|
2493
|
+
fail(e);
|
|
2494
|
+
}
|
|
2495
|
+
return Effect15.sync(cleanup);
|
|
2496
|
+
})
|
|
2497
|
+
);
|
|
2181
2498
|
return {
|
|
2182
2499
|
publish: (event, urls) => Effect15.gen(function* () {
|
|
2183
|
-
const results = yield* Effect15.forEach(
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2500
|
+
const results = yield* Effect15.forEach(
|
|
2501
|
+
urls,
|
|
2502
|
+
(url) => Effect15.result(
|
|
2503
|
+
withRelay(
|
|
2504
|
+
url,
|
|
2505
|
+
(relay) => Effect15.tryPromise({
|
|
2506
|
+
try: () => relay.publish(event),
|
|
2507
|
+
catch: (e) => new RelayError({
|
|
2508
|
+
message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2509
|
+
url,
|
|
2510
|
+
cause: e
|
|
2511
|
+
})
|
|
2512
|
+
}).pipe(
|
|
2513
|
+
Effect15.timeoutOrElse({
|
|
2514
|
+
duration: "10 seconds",
|
|
2515
|
+
onTimeout: () => Effect15.fail(
|
|
2516
|
+
new RelayError({ message: `Publish to ${url} timed out`, url })
|
|
2517
|
+
)
|
|
2518
|
+
})
|
|
2519
|
+
)
|
|
2520
|
+
)
|
|
2521
|
+
),
|
|
2522
|
+
{ concurrency: "unbounded" }
|
|
2523
|
+
);
|
|
2194
2524
|
const failures = results.filter((r) => r._tag === "Failure");
|
|
2195
2525
|
if (failures.length === urls.length && urls.length > 0) {
|
|
2196
2526
|
return yield* new RelayError({
|
|
@@ -2199,90 +2529,104 @@ function createRelayHandle() {
|
|
|
2199
2529
|
}
|
|
2200
2530
|
}),
|
|
2201
2531
|
fetchEvents: (ids, url) => Effect15.gen(function* () {
|
|
2202
|
-
if (ids.length === 0)
|
|
2203
|
-
return [];
|
|
2532
|
+
if (ids.length === 0) return [];
|
|
2204
2533
|
return yield* collectEvents(url, [{ ids }]);
|
|
2205
2534
|
}),
|
|
2206
2535
|
fetchByFilter: (filter, url) => collectEvents(url, [filter]),
|
|
2207
|
-
subscribe: (filter, url, onEvent) => withRelay(
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
sub
|
|
2235
|
-
ws
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
return;
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
try {
|
|
2262
|
-
sub = relay.subscribe([filter], {
|
|
2263
|
-
onevent() {},
|
|
2264
|
-
oneose() {}
|
|
2265
|
-
});
|
|
2266
|
-
ws = relay._ws || relay.ws;
|
|
2267
|
-
if (!ws) {
|
|
2536
|
+
subscribe: (filter, url, onEvent) => withRelay(
|
|
2537
|
+
url,
|
|
2538
|
+
(relay) => Effect15.acquireRelease(
|
|
2539
|
+
Effect15.try({
|
|
2540
|
+
try: () => relay.subscribe([filter], {
|
|
2541
|
+
onevent(evt) {
|
|
2542
|
+
onEvent(evt);
|
|
2543
|
+
},
|
|
2544
|
+
oneose() {
|
|
2545
|
+
}
|
|
2546
|
+
}),
|
|
2547
|
+
catch: (e) => new RelayError({
|
|
2548
|
+
message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2549
|
+
url,
|
|
2550
|
+
cause: e
|
|
2551
|
+
})
|
|
2552
|
+
}),
|
|
2553
|
+
(sub) => Effect15.sync(() => {
|
|
2554
|
+
sub.close();
|
|
2555
|
+
})
|
|
2556
|
+
).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)
|
|
2557
|
+
),
|
|
2558
|
+
sendNegMsg: (url, subId, filter, msgHex) => withRelay(
|
|
2559
|
+
url,
|
|
2560
|
+
(relay) => Effect15.callback((resume) => {
|
|
2561
|
+
let settled = false;
|
|
2562
|
+
let timer;
|
|
2563
|
+
let sub;
|
|
2564
|
+
let ws;
|
|
2565
|
+
const cleanup = () => {
|
|
2566
|
+
settled = true;
|
|
2567
|
+
if (timer !== void 0) {
|
|
2568
|
+
clearTimeout(timer);
|
|
2569
|
+
timer = void 0;
|
|
2570
|
+
}
|
|
2571
|
+
sub?.close();
|
|
2572
|
+
sub = void 0;
|
|
2573
|
+
ws?.removeEventListener("message", handler);
|
|
2574
|
+
ws = void 0;
|
|
2575
|
+
};
|
|
2576
|
+
const fail = (e) => resume(
|
|
2577
|
+
Effect15.fail(
|
|
2578
|
+
new RelayError({
|
|
2579
|
+
message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2580
|
+
url,
|
|
2581
|
+
cause: e
|
|
2582
|
+
})
|
|
2583
|
+
)
|
|
2584
|
+
);
|
|
2585
|
+
const handler = (msg) => {
|
|
2586
|
+
if (settled || typeof msg.data !== "string") return;
|
|
2587
|
+
const frameOpt = parseNegMessageFrame(msg.data);
|
|
2588
|
+
if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId) return;
|
|
2589
|
+
const frame = frameOpt.value;
|
|
2268
2590
|
cleanup();
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2591
|
+
if (frame[0] === "NEG-MSG") {
|
|
2592
|
+
resume(
|
|
2593
|
+
Effect15.succeed({
|
|
2594
|
+
msgHex: frame[2],
|
|
2595
|
+
haveIds: [],
|
|
2596
|
+
needIds: []
|
|
2597
|
+
})
|
|
2598
|
+
);
|
|
2274
2599
|
return;
|
|
2600
|
+
}
|
|
2601
|
+
fail(new Error(`NEG-ERR: ${frame[2]}`));
|
|
2602
|
+
};
|
|
2603
|
+
try {
|
|
2604
|
+
sub = relay.subscribe([filter], {
|
|
2605
|
+
onevent() {
|
|
2606
|
+
},
|
|
2607
|
+
oneose() {
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
ws = relay._ws || relay.ws;
|
|
2611
|
+
if (!ws) {
|
|
2612
|
+
cleanup();
|
|
2613
|
+
fail(new Error("Cannot access relay WebSocket"));
|
|
2614
|
+
return Effect15.succeed(void 0);
|
|
2615
|
+
}
|
|
2616
|
+
timer = setTimeout(() => {
|
|
2617
|
+
if (settled) return;
|
|
2618
|
+
cleanup();
|
|
2619
|
+
fail(new Error("NIP-77 negotiation timeout"));
|
|
2620
|
+
}, 3e4);
|
|
2621
|
+
ws.addEventListener("message", handler);
|
|
2622
|
+
ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
|
|
2623
|
+
} catch (e) {
|
|
2275
2624
|
cleanup();
|
|
2276
|
-
fail(
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
cleanup();
|
|
2282
|
-
fail(e);
|
|
2283
|
-
}
|
|
2284
|
-
return Effect15.sync(cleanup);
|
|
2285
|
-
})),
|
|
2625
|
+
fail(e);
|
|
2626
|
+
}
|
|
2627
|
+
return Effect15.sync(cleanup);
|
|
2628
|
+
})
|
|
2629
|
+
),
|
|
2286
2630
|
closeAll: () => ScopedCache.invalidateAll(connections),
|
|
2287
2631
|
getStatus: () => ({ connectedUrls: [...connectedUrls] }),
|
|
2288
2632
|
subscribeStatus: (callback) => {
|
|
@@ -2294,10 +2638,10 @@ function createRelayHandle() {
|
|
|
2294
2638
|
}
|
|
2295
2639
|
|
|
2296
2640
|
// src/layers/RelayLive.ts
|
|
2297
|
-
var RelayLive =
|
|
2641
|
+
var RelayLive = Layer5.effect(Relay, createRelayHandle());
|
|
2298
2642
|
|
|
2299
2643
|
// src/layers/GiftWrapLive.ts
|
|
2300
|
-
import { Effect as Effect17, Layer as
|
|
2644
|
+
import { Effect as Effect17, Layer as Layer6 } from "effect";
|
|
2301
2645
|
|
|
2302
2646
|
// src/sync/gift-wrap.ts
|
|
2303
2647
|
import { Effect as Effect16 } from "effect";
|
|
@@ -2334,14 +2678,17 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
|
2334
2678
|
}
|
|
2335
2679
|
|
|
2336
2680
|
// src/layers/GiftWrapLive.ts
|
|
2337
|
-
var GiftWrapLive =
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2681
|
+
var GiftWrapLive = Layer6.effect(
|
|
2682
|
+
GiftWrap3,
|
|
2683
|
+
Effect17.gen(function* () {
|
|
2684
|
+
const identity = yield* Identity;
|
|
2685
|
+
const epochStore = yield* EpochStore;
|
|
2686
|
+
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2687
|
+
})
|
|
2688
|
+
);
|
|
2342
2689
|
|
|
2343
2690
|
// src/layers/PublishQueueLive.ts
|
|
2344
|
-
import { Effect as Effect19, Layer as
|
|
2691
|
+
import { Effect as Effect19, Layer as Layer7 } from "effect";
|
|
2345
2692
|
|
|
2346
2693
|
// src/sync/publish-queue.ts
|
|
2347
2694
|
import { Effect as Effect18, Ref as Ref4 } from "effect";
|
|
@@ -2352,12 +2699,11 @@ function persist(storage, pending) {
|
|
|
2352
2699
|
function createPublishQueue(storage, relay) {
|
|
2353
2700
|
return Effect18.gen(function* () {
|
|
2354
2701
|
const stored = yield* storage.getMeta(META_KEY);
|
|
2355
|
-
const initial = Array.isArray(stored) ? new Set(stored) : new Set;
|
|
2702
|
+
const initial = Array.isArray(stored) ? new Set(stored) : /* @__PURE__ */ new Set();
|
|
2356
2703
|
const pendingRef = yield* Ref4.make(initial);
|
|
2357
|
-
const listeners = new Set;
|
|
2704
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2358
2705
|
const notify = (pending) => {
|
|
2359
|
-
for (const listener of listeners)
|
|
2360
|
-
listener(pending.size);
|
|
2706
|
+
for (const listener of listeners) listener(pending.size);
|
|
2361
2707
|
};
|
|
2362
2708
|
return {
|
|
2363
2709
|
enqueue: (eventId) => Effect18.gen(function* () {
|
|
@@ -2371,13 +2717,11 @@ function createPublishQueue(storage, relay) {
|
|
|
2371
2717
|
}),
|
|
2372
2718
|
flush: (relayUrls) => Effect18.gen(function* () {
|
|
2373
2719
|
const pending = yield* Ref4.get(pendingRef);
|
|
2374
|
-
if (pending.size === 0)
|
|
2375
|
-
|
|
2376
|
-
const succeeded = new Set;
|
|
2720
|
+
if (pending.size === 0) return;
|
|
2721
|
+
const succeeded = /* @__PURE__ */ new Set();
|
|
2377
2722
|
let consecutiveFailures = 0;
|
|
2378
2723
|
for (const eventId of pending) {
|
|
2379
|
-
if (consecutiveFailures >= 3)
|
|
2380
|
-
break;
|
|
2724
|
+
if (consecutiveFailures >= 3) break;
|
|
2381
2725
|
const gw = yield* storage.getGiftWrap(eventId);
|
|
2382
2726
|
if (!gw || !gw.event) {
|
|
2383
2727
|
succeeded.add(eventId);
|
|
@@ -2387,7 +2731,6 @@ function createPublishQueue(storage, relay) {
|
|
|
2387
2731
|
const result = yield* Effect18.result(relay.publish(gw.event, relayUrls));
|
|
2388
2732
|
if (result._tag === "Success") {
|
|
2389
2733
|
succeeded.add(eventId);
|
|
2390
|
-
yield* storage.stripGiftWrapBlob(eventId);
|
|
2391
2734
|
consecutiveFailures = 0;
|
|
2392
2735
|
} else {
|
|
2393
2736
|
consecutiveFailures++;
|
|
@@ -2415,27 +2758,29 @@ function createPublishQueue(storage, relay) {
|
|
|
2415
2758
|
}
|
|
2416
2759
|
|
|
2417
2760
|
// src/layers/PublishQueueLive.ts
|
|
2418
|
-
var PublishQueueLive =
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2761
|
+
var PublishQueueLive = Layer7.effect(
|
|
2762
|
+
PublishQueue,
|
|
2763
|
+
Effect19.gen(function* () {
|
|
2764
|
+
const storage = yield* Storage;
|
|
2765
|
+
const relay = yield* Relay;
|
|
2766
|
+
return yield* createPublishQueue(storage, relay);
|
|
2767
|
+
})
|
|
2768
|
+
);
|
|
2423
2769
|
|
|
2424
2770
|
// src/layers/SyncStatusLive.ts
|
|
2425
|
-
import { Layer as
|
|
2771
|
+
import { Layer as Layer8 } from "effect";
|
|
2426
2772
|
|
|
2427
2773
|
// src/sync/sync-status.ts
|
|
2428
2774
|
import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
2429
2775
|
function createSyncStatusHandle() {
|
|
2430
2776
|
return Effect20.gen(function* () {
|
|
2431
2777
|
const ref = yield* SubscriptionRef.make("idle");
|
|
2432
|
-
const listeners = new Set;
|
|
2778
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2433
2779
|
return {
|
|
2434
2780
|
get: () => SubscriptionRef.get(ref),
|
|
2435
2781
|
set: (status) => Effect20.gen(function* () {
|
|
2436
2782
|
yield* SubscriptionRef.set(ref, status);
|
|
2437
|
-
for (const listener of listeners)
|
|
2438
|
-
listener(status);
|
|
2783
|
+
for (const listener of listeners) listener(status);
|
|
2439
2784
|
}),
|
|
2440
2785
|
subscribe: (callback) => {
|
|
2441
2786
|
listeners.add(callback);
|
|
@@ -2446,12 +2791,11 @@ function createSyncStatusHandle() {
|
|
|
2446
2791
|
}
|
|
2447
2792
|
|
|
2448
2793
|
// src/layers/SyncStatusLive.ts
|
|
2449
|
-
var SyncStatusLive =
|
|
2794
|
+
var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
|
|
2450
2795
|
|
|
2451
2796
|
// src/layers/TablinumLive.ts
|
|
2452
2797
|
function reportSyncError(onSyncError, error) {
|
|
2453
|
-
if (!onSyncError)
|
|
2454
|
-
return;
|
|
2798
|
+
if (!onSyncError) return;
|
|
2455
2799
|
onSyncError(error instanceof Error ? error : new Error(String(error)));
|
|
2456
2800
|
}
|
|
2457
2801
|
function mapMemberRecord(record) {
|
|
@@ -2459,239 +2803,365 @@ function mapMemberRecord(record) {
|
|
|
2459
2803
|
id: record.id,
|
|
2460
2804
|
addedAt: record.addedAt,
|
|
2461
2805
|
addedInEpoch: record.addedInEpoch,
|
|
2462
|
-
...record.name !==
|
|
2463
|
-
...record.picture !==
|
|
2464
|
-
...record.about !==
|
|
2465
|
-
...record.nip05 !==
|
|
2466
|
-
...record.removedAt !==
|
|
2467
|
-
...record.removedInEpoch !==
|
|
2806
|
+
...record.name !== void 0 ? { name: record.name } : {},
|
|
2807
|
+
...record.picture !== void 0 ? { picture: record.picture } : {},
|
|
2808
|
+
...record.about !== void 0 ? { about: record.about } : {},
|
|
2809
|
+
...record.nip05 !== void 0 ? { nip05: record.nip05 } : {},
|
|
2810
|
+
...record.removedAt !== void 0 ? { removedAt: record.removedAt } : {},
|
|
2811
|
+
...record.removedInEpoch !== void 0 ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2468
2812
|
};
|
|
2469
2813
|
}
|
|
2470
|
-
var IdentityWithDeps = IdentityLive.pipe(
|
|
2471
|
-
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
var
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
const
|
|
2496
|
-
const
|
|
2497
|
-
const
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2814
|
+
var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
|
|
2815
|
+
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2816
|
+
Layer9.provide(IdentityWithDeps),
|
|
2817
|
+
Layer9.provide(StorageLive)
|
|
2818
|
+
);
|
|
2819
|
+
var GiftWrapWithDeps = GiftWrapLive.pipe(
|
|
2820
|
+
Layer9.provide(IdentityWithDeps),
|
|
2821
|
+
Layer9.provide(EpochStoreWithDeps)
|
|
2822
|
+
);
|
|
2823
|
+
var PublishQueueWithDeps = PublishQueueLive.pipe(
|
|
2824
|
+
Layer9.provide(StorageLive),
|
|
2825
|
+
Layer9.provide(RelayLive)
|
|
2826
|
+
);
|
|
2827
|
+
var AllServicesLive = Layer9.mergeAll(
|
|
2828
|
+
IdentityWithDeps,
|
|
2829
|
+
EpochStoreWithDeps,
|
|
2830
|
+
StorageLive,
|
|
2831
|
+
RelayLive,
|
|
2832
|
+
GiftWrapWithDeps,
|
|
2833
|
+
PublishQueueWithDeps,
|
|
2834
|
+
SyncStatusLive
|
|
2835
|
+
);
|
|
2836
|
+
var TablinumLive = Layer9.effect(
|
|
2837
|
+
Tablinum,
|
|
2838
|
+
Effect21.gen(function* () {
|
|
2839
|
+
const config = yield* Config;
|
|
2840
|
+
const identity = yield* Identity;
|
|
2841
|
+
const epochStore = yield* EpochStore;
|
|
2842
|
+
const storage = yield* Storage;
|
|
2843
|
+
const relay = yield* Relay;
|
|
2844
|
+
const giftWrap = yield* GiftWrap3;
|
|
2845
|
+
const publishQueue = yield* PublishQueue;
|
|
2846
|
+
const syncStatus = yield* SyncStatus;
|
|
2847
|
+
const scope = yield* Effect21.scope;
|
|
2848
|
+
const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
|
|
2849
|
+
const pubsub = yield* PubSub2.unbounded();
|
|
2850
|
+
const replayingRef = yield* Ref5.make(false);
|
|
2851
|
+
const closedRef = yield* Ref5.make(false);
|
|
2852
|
+
const watchCtx = { pubsub, replayingRef };
|
|
2853
|
+
const schemaEntries = Object.entries(config.schema);
|
|
2854
|
+
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2855
|
+
const knownCollections = new Map(
|
|
2856
|
+
allSchemaEntries.map(([, def]) => [def.name, def.eventRetention])
|
|
2857
|
+
);
|
|
2858
|
+
let notifyAuthor;
|
|
2859
|
+
const syncHandle = createSyncHandle(
|
|
2860
|
+
storage,
|
|
2861
|
+
giftWrap,
|
|
2862
|
+
relay,
|
|
2863
|
+
publishQueue,
|
|
2864
|
+
syncStatus,
|
|
2865
|
+
watchCtx,
|
|
2866
|
+
config.relays,
|
|
2867
|
+
knownCollections,
|
|
2868
|
+
epochStore,
|
|
2869
|
+
identity.privateKey,
|
|
2870
|
+
identity.publicKey,
|
|
2871
|
+
scope,
|
|
2872
|
+
config.logLevel,
|
|
2873
|
+
config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : void 0,
|
|
2874
|
+
(pubkey) => notifyAuthor?.(pubkey),
|
|
2875
|
+
config.onRemoved,
|
|
2876
|
+
config.onMembersChanged
|
|
2877
|
+
);
|
|
2878
|
+
const onWrite = (event) => Effect21.gen(function* () {
|
|
2879
|
+
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2880
|
+
const dTag = `${event.collection}:${event.recordId}`;
|
|
2881
|
+
const wrapResult = yield* Effect21.result(
|
|
2882
|
+
giftWrap.wrap({
|
|
2883
|
+
kind: 1,
|
|
2884
|
+
content,
|
|
2885
|
+
tags: [["d", dTag]],
|
|
2886
|
+
created_at: Math.floor(event.createdAt / 1e3)
|
|
2887
|
+
})
|
|
2888
|
+
);
|
|
2889
|
+
if (wrapResult._tag === "Failure") {
|
|
2890
|
+
reportSyncError(config.onSyncError, wrapResult.failure);
|
|
2891
|
+
return;
|
|
2517
2892
|
}
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
yield* notifyChange(watchCtx, {
|
|
2536
|
-
collection: "_members",
|
|
2537
|
-
recordId: record.id,
|
|
2538
|
-
kind: existing ? "update" : "create"
|
|
2893
|
+
const gw = wrapResult.success;
|
|
2894
|
+
yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
2895
|
+
yield* Effect21.forkIn(
|
|
2896
|
+
Effect21.gen(function* () {
|
|
2897
|
+
const publishResult = yield* Effect21.result(
|
|
2898
|
+
syncHandle.publishLocal({
|
|
2899
|
+
id: gw.id,
|
|
2900
|
+
event: gw,
|
|
2901
|
+
createdAt: gw.created_at
|
|
2902
|
+
})
|
|
2903
|
+
);
|
|
2904
|
+
if (publishResult._tag === "Failure") {
|
|
2905
|
+
reportSyncError(config.onSyncError, publishResult.failure);
|
|
2906
|
+
}
|
|
2907
|
+
}),
|
|
2908
|
+
scope
|
|
2909
|
+
);
|
|
2539
2910
|
});
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
...profileOpt.value
|
|
2562
|
-
});
|
|
2563
|
-
yield* notifyChange(watchCtx, {
|
|
2564
|
-
collection: "_members",
|
|
2565
|
-
recordId: pubkey,
|
|
2566
|
-
kind: "update"
|
|
2567
|
-
});
|
|
2568
|
-
config.onMembersChanged?.();
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
}).pipe(Effect21.ignore, Effect21.forkIn(scope)));
|
|
2572
|
-
};
|
|
2573
|
-
const handles = new Map;
|
|
2574
|
-
for (const [, def] of allSchemaEntries) {
|
|
2575
|
-
const validator = buildValidator(def.name, def);
|
|
2576
|
-
const partialValidator = buildPartialValidator(def.name, def);
|
|
2577
|
-
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite);
|
|
2578
|
-
handles.set(def.name, handle);
|
|
2579
|
-
}
|
|
2580
|
-
yield* syncHandle.startSubscription();
|
|
2581
|
-
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2582
|
-
if (!selfMember) {
|
|
2583
|
-
yield* putMemberRecord({
|
|
2584
|
-
id: identity.publicKey,
|
|
2585
|
-
addedAt: Date.now(),
|
|
2586
|
-
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2911
|
+
const knownAuthors = /* @__PURE__ */ new Set();
|
|
2912
|
+
const putMemberRecord = (record) => Effect21.gen(function* () {
|
|
2913
|
+
const existing = yield* storage.getRecord("_members", record.id);
|
|
2914
|
+
const event = {
|
|
2915
|
+
id: uuidv7(),
|
|
2916
|
+
collection: "_members",
|
|
2917
|
+
recordId: record.id,
|
|
2918
|
+
kind: existing ? "u" : "c",
|
|
2919
|
+
data: record,
|
|
2920
|
+
createdAt: Date.now(),
|
|
2921
|
+
author: identity.publicKey
|
|
2922
|
+
};
|
|
2923
|
+
yield* storage.putEvent(event);
|
|
2924
|
+
yield* applyEvent(storage, event);
|
|
2925
|
+
yield* onWrite(event);
|
|
2926
|
+
yield* notifyChange(watchCtx, {
|
|
2927
|
+
collection: "_members",
|
|
2928
|
+
recordId: record.id,
|
|
2929
|
+
kind: existing ? "update" : "create"
|
|
2930
|
+
});
|
|
2931
|
+
config.onMembersChanged?.();
|
|
2587
2932
|
});
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2933
|
+
notifyAuthor = (pubkey) => {
|
|
2934
|
+
if (knownAuthors.has(pubkey)) return;
|
|
2935
|
+
knownAuthors.add(pubkey);
|
|
2936
|
+
Effect21.runFork(
|
|
2937
|
+
Effect21.gen(function* () {
|
|
2938
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
2939
|
+
if (!existing) {
|
|
2940
|
+
yield* putMemberRecord({
|
|
2941
|
+
id: pubkey,
|
|
2942
|
+
addedAt: Date.now(),
|
|
2943
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(
|
|
2947
|
+
Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none()))
|
|
2948
|
+
);
|
|
2949
|
+
if (Option9.isSome(profileOpt)) {
|
|
2950
|
+
const current = yield* storage.getRecord("_members", pubkey);
|
|
2951
|
+
if (current) {
|
|
2952
|
+
yield* storage.putRecord("_members", {
|
|
2953
|
+
...current,
|
|
2954
|
+
...profileOpt.value
|
|
2955
|
+
});
|
|
2956
|
+
yield* notifyChange(watchCtx, {
|
|
2957
|
+
collection: "_members",
|
|
2958
|
+
recordId: pubkey,
|
|
2959
|
+
kind: "update"
|
|
2960
|
+
});
|
|
2961
|
+
config.onMembersChanged?.();
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope))
|
|
2965
|
+
);
|
|
2966
|
+
};
|
|
2967
|
+
const handles = /* @__PURE__ */ new Map();
|
|
2968
|
+
for (const [, def] of allSchemaEntries) {
|
|
2969
|
+
const validator = buildValidator(def.name, def);
|
|
2970
|
+
const partialValidator = buildPartialValidator(def.name, def);
|
|
2971
|
+
const handle = createCollectionHandle(
|
|
2972
|
+
def,
|
|
2973
|
+
storage,
|
|
2974
|
+
watchCtx,
|
|
2975
|
+
validator,
|
|
2976
|
+
partialValidator,
|
|
2977
|
+
uuidv7,
|
|
2978
|
+
identity.publicKey,
|
|
2979
|
+
onWrite,
|
|
2980
|
+
config.logLevel
|
|
2981
|
+
);
|
|
2982
|
+
handles.set(def.name, handle);
|
|
2983
|
+
}
|
|
2984
|
+
yield* syncHandle.startSubscription();
|
|
2985
|
+
yield* Effect21.logInfo("Tablinum ready", {
|
|
2986
|
+
dbName: config.dbName,
|
|
2987
|
+
collections: schemaEntries.map(([name]) => name),
|
|
2988
|
+
relays: config.relays
|
|
2989
|
+
});
|
|
2990
|
+
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2991
|
+
if (!selfMember) {
|
|
2634
2992
|
yield* putMemberRecord({
|
|
2635
|
-
id:
|
|
2993
|
+
id: identity.publicKey,
|
|
2636
2994
|
addedAt: Date.now(),
|
|
2637
|
-
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2638
|
-
...existing ? { removedAt: undefined, removedInEpoch: undefined } : {}
|
|
2639
|
-
});
|
|
2640
|
-
})),
|
|
2641
|
-
removeMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
|
|
2642
|
-
const allMembers = yield* storage.getAllRecords("_members");
|
|
2643
|
-
const activeMembers = allMembers.filter((member) => !member.removedAt && member.id !== pubkey);
|
|
2644
|
-
const activePubkeys = activeMembers.map((member) => member.id);
|
|
2645
|
-
const result = createRotation(epochStore, identity.privateKey, identity.publicKey, activePubkeys, [pubkey]);
|
|
2646
|
-
addEpoch(epochStore, result.epoch);
|
|
2647
|
-
epochStore.currentEpochId = result.epoch.id;
|
|
2648
|
-
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
2649
|
-
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
2650
|
-
yield* putMemberRecord({
|
|
2651
|
-
...memberRecord ?? {
|
|
2652
|
-
id: pubkey,
|
|
2653
|
-
addedAt: 0,
|
|
2654
|
-
addedInEpoch: EpochId("epoch-0")
|
|
2655
|
-
},
|
|
2656
|
-
removedAt: Date.now(),
|
|
2657
|
-
removedInEpoch: result.epoch.id
|
|
2995
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2658
2996
|
});
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2997
|
+
}
|
|
2998
|
+
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
2999
|
+
const ensureOpen = (effect) => withLog(
|
|
3000
|
+
Effect21.gen(function* () {
|
|
3001
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3002
|
+
return yield* new StorageError({ message: "Database is closed" });
|
|
3003
|
+
}
|
|
3004
|
+
return yield* effect;
|
|
3005
|
+
})
|
|
3006
|
+
);
|
|
3007
|
+
const ensureSyncOpen = (effect) => withLog(
|
|
3008
|
+
Effect21.gen(function* () {
|
|
3009
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3010
|
+
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
3011
|
+
}
|
|
3012
|
+
return yield* effect;
|
|
3013
|
+
})
|
|
3014
|
+
);
|
|
3015
|
+
const dbHandle = {
|
|
3016
|
+
collection: (name) => {
|
|
3017
|
+
const handle = handles.get(name);
|
|
3018
|
+
if (!handle) throw new Error(`Collection "${name}" not found in schema`);
|
|
3019
|
+
return handle;
|
|
3020
|
+
},
|
|
3021
|
+
publicKey: identity.publicKey,
|
|
3022
|
+
members: handles.get("_members"),
|
|
3023
|
+
exportKey: () => identity.exportKey(),
|
|
3024
|
+
exportInvite: () => ({
|
|
3025
|
+
epochKeys: [...exportEpochKeys(epochStore)],
|
|
3026
|
+
relays: [...config.relays],
|
|
3027
|
+
dbName: config.dbName
|
|
3028
|
+
}),
|
|
3029
|
+
close: () => withLog(
|
|
3030
|
+
Effect21.gen(function* () {
|
|
3031
|
+
if (yield* Ref5.get(closedRef)) return;
|
|
3032
|
+
yield* Ref5.set(closedRef, true);
|
|
3033
|
+
syncHandle.stopHealing();
|
|
3034
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3035
|
+
})
|
|
3036
|
+
),
|
|
3037
|
+
rebuild: () => ensureOpen(
|
|
3038
|
+
rebuild(
|
|
3039
|
+
storage,
|
|
3040
|
+
allSchemaEntries.map(([, def]) => def.name)
|
|
3041
|
+
)
|
|
3042
|
+
),
|
|
3043
|
+
sync: () => ensureSyncOpen(
|
|
3044
|
+
syncHandle.sync().pipe(
|
|
3045
|
+
Effect21.tap(
|
|
3046
|
+
() => Effect21.sync(() => {
|
|
3047
|
+
syncHandle.startHealing();
|
|
3048
|
+
})
|
|
3049
|
+
)
|
|
3050
|
+
)
|
|
3051
|
+
),
|
|
3052
|
+
getSyncStatus: () => syncStatus.get(),
|
|
3053
|
+
subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
|
|
3054
|
+
pendingCount: () => publishQueue.size(),
|
|
3055
|
+
subscribePendingCount: (callback) => publishQueue.subscribe(callback),
|
|
3056
|
+
getRelayStatus: () => relay.getStatus(),
|
|
3057
|
+
subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
|
|
3058
|
+
addMember: (pubkey) => ensureOpen(
|
|
3059
|
+
Effect21.gen(function* () {
|
|
3060
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
3061
|
+
if (existing && !existing.removedAt) return;
|
|
3062
|
+
yield* putMemberRecord({
|
|
3063
|
+
id: pubkey,
|
|
3064
|
+
addedAt: Date.now(),
|
|
3065
|
+
addedInEpoch: getCurrentEpoch(epochStore).id,
|
|
3066
|
+
...existing ? { removedAt: void 0, removedInEpoch: void 0 } : {}
|
|
3067
|
+
});
|
|
3068
|
+
})
|
|
3069
|
+
),
|
|
3070
|
+
removeMember: (pubkey) => ensureOpen(
|
|
3071
|
+
Effect21.gen(function* () {
|
|
3072
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
3073
|
+
const activeMembers = allMembers.filter(
|
|
3074
|
+
(member) => !member.removedAt && member.id !== pubkey
|
|
3075
|
+
);
|
|
3076
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
3077
|
+
const result = createRotation(
|
|
3078
|
+
epochStore,
|
|
3079
|
+
identity.privateKey,
|
|
3080
|
+
identity.publicKey,
|
|
3081
|
+
activePubkeys,
|
|
3082
|
+
[pubkey]
|
|
3083
|
+
);
|
|
3084
|
+
addEpoch(epochStore, result.epoch);
|
|
3085
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
3086
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
3087
|
+
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
3088
|
+
yield* putMemberRecord({
|
|
3089
|
+
...memberRecord ?? {
|
|
3090
|
+
id: pubkey,
|
|
3091
|
+
addedAt: 0,
|
|
3092
|
+
addedInEpoch: EpochId("epoch-0")
|
|
3093
|
+
},
|
|
3094
|
+
removedAt: Date.now(),
|
|
3095
|
+
removedInEpoch: result.epoch.id
|
|
3096
|
+
});
|
|
3097
|
+
yield* Effect21.forEach(
|
|
3098
|
+
result.wrappedEvents,
|
|
3099
|
+
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
3100
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3101
|
+
Effect21.ignore
|
|
3102
|
+
),
|
|
3103
|
+
{ discard: true }
|
|
3104
|
+
);
|
|
3105
|
+
yield* Effect21.forEach(
|
|
3106
|
+
result.removalNotices,
|
|
3107
|
+
(notice) => relay.publish(notice, [...config.relays]).pipe(
|
|
3108
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3109
|
+
Effect21.ignore
|
|
3110
|
+
),
|
|
3111
|
+
{ discard: true }
|
|
3112
|
+
);
|
|
3113
|
+
yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
|
|
3114
|
+
})
|
|
3115
|
+
),
|
|
3116
|
+
getMembers: () => ensureOpen(
|
|
3117
|
+
Effect21.gen(function* () {
|
|
3118
|
+
const allRecords = yield* storage.getAllRecords("_members");
|
|
3119
|
+
return allRecords.filter((record) => !record._d).map(mapMemberRecord);
|
|
3120
|
+
})
|
|
3121
|
+
),
|
|
3122
|
+
getProfile: () => ensureOpen(
|
|
3123
|
+
Effect21.gen(function* () {
|
|
3124
|
+
const record = yield* storage.getRecord("_members", identity.publicKey);
|
|
3125
|
+
if (!record) return {};
|
|
3126
|
+
const profile = {};
|
|
3127
|
+
if (record.name !== void 0) profile.name = record.name;
|
|
3128
|
+
if (record.picture !== void 0) profile.picture = record.picture;
|
|
3129
|
+
if (record.about !== void 0) profile.about = record.about;
|
|
3130
|
+
if (record.nip05 !== void 0) profile.nip05 = record.nip05;
|
|
3131
|
+
return profile;
|
|
3132
|
+
})
|
|
3133
|
+
),
|
|
3134
|
+
setProfile: (profile) => ensureOpen(
|
|
3135
|
+
Effect21.gen(function* () {
|
|
3136
|
+
const existing = yield* storage.getRecord("_members", identity.publicKey);
|
|
3137
|
+
if (!existing) {
|
|
3138
|
+
return yield* new ValidationError({ message: "Current user is not a member" });
|
|
3139
|
+
}
|
|
3140
|
+
const { _d, _u, _a, _e, ...memberFields } = existing;
|
|
3141
|
+
yield* putMemberRecord({ ...memberFields, ...profile });
|
|
3142
|
+
})
|
|
3143
|
+
)
|
|
3144
|
+
};
|
|
3145
|
+
return dbHandle;
|
|
3146
|
+
}).pipe(Effect21.withLogSpan("tablinum.init"))
|
|
3147
|
+
).pipe(Layer9.provide(AllServicesLive));
|
|
2693
3148
|
|
|
2694
3149
|
// src/db/create-tablinum.ts
|
|
3150
|
+
function resolveLogLevel(input) {
|
|
3151
|
+
if (input === void 0 || input === "none") return "None";
|
|
3152
|
+
switch (input) {
|
|
3153
|
+
case "debug":
|
|
3154
|
+
return "Debug";
|
|
3155
|
+
case "info":
|
|
3156
|
+
return "Info";
|
|
3157
|
+
case "warning":
|
|
3158
|
+
return "Warn";
|
|
3159
|
+
case "error":
|
|
3160
|
+
return "Error";
|
|
3161
|
+
default:
|
|
3162
|
+
return input;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
2695
3165
|
function validateConfig(config) {
|
|
2696
3166
|
return Effect22.gen(function* () {
|
|
2697
3167
|
if (Object.keys(config.schema).length === 0) {
|
|
@@ -2705,22 +3175,25 @@ function createTablinum(config) {
|
|
|
2705
3175
|
return Effect22.gen(function* () {
|
|
2706
3176
|
yield* validateConfig(config);
|
|
2707
3177
|
const runtimeConfig = yield* resolveRuntimeConfig(config);
|
|
3178
|
+
const logLevel = resolveLogLevel(config.logLevel);
|
|
2708
3179
|
const configValue = {
|
|
2709
3180
|
...runtimeConfig,
|
|
2710
3181
|
schema: config.schema,
|
|
3182
|
+
logLevel,
|
|
2711
3183
|
onSyncError: config.onSyncError,
|
|
2712
3184
|
onRemoved: config.onRemoved,
|
|
2713
3185
|
onMembersChanged: config.onMembersChanged
|
|
2714
3186
|
};
|
|
2715
|
-
const configLayer =
|
|
2716
|
-
const
|
|
2717
|
-
const
|
|
3187
|
+
const configLayer = Layer10.succeed(Config, configValue);
|
|
3188
|
+
const logLayer = Layer10.succeed(References4.MinimumLogLevel, logLevel);
|
|
3189
|
+
const fullLayer = TablinumLive.pipe(Layer10.provide(configLayer), Layer10.provide(logLayer));
|
|
3190
|
+
const ctx = yield* Layer10.build(fullLayer);
|
|
2718
3191
|
return ServiceMap10.get(ctx, Tablinum);
|
|
2719
3192
|
});
|
|
2720
3193
|
}
|
|
2721
3194
|
|
|
2722
3195
|
// src/svelte/collection.svelte.ts
|
|
2723
|
-
import { Effect as Effect24, Fiber, Option as Option11, Stream as Stream4 } from "effect";
|
|
3196
|
+
import { Effect as Effect24, Fiber, Option as Option11, References as References5, Stream as Stream4 } from "effect";
|
|
2724
3197
|
|
|
2725
3198
|
// src/svelte/deferred.ts
|
|
2726
3199
|
function createDeferred() {
|
|
@@ -2735,14 +3208,12 @@ function createDeferred() {
|
|
|
2735
3208
|
promise,
|
|
2736
3209
|
settled: () => settled,
|
|
2737
3210
|
resolve: (value) => {
|
|
2738
|
-
if (settled)
|
|
2739
|
-
return;
|
|
3211
|
+
if (settled) return;
|
|
2740
3212
|
settled = true;
|
|
2741
3213
|
resolvePromise(value);
|
|
2742
3214
|
},
|
|
2743
3215
|
reject: (reason) => {
|
|
2744
|
-
if (settled)
|
|
2745
|
-
return;
|
|
3216
|
+
if (settled) return;
|
|
2746
3217
|
settled = true;
|
|
2747
3218
|
rejectPromise(reason);
|
|
2748
3219
|
}
|
|
@@ -2764,7 +3235,9 @@ function wrapQueryBuilder(getBuilder, touchVersion, ready) {
|
|
|
2764
3235
|
},
|
|
2765
3236
|
first: () => {
|
|
2766
3237
|
touchVersion();
|
|
2767
|
-
return ready.then(
|
|
3238
|
+
return ready.then(
|
|
3239
|
+
() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull))
|
|
3240
|
+
);
|
|
2768
3241
|
},
|
|
2769
3242
|
count: () => {
|
|
2770
3243
|
touchVersion();
|
|
@@ -2796,7 +3269,9 @@ function wrapOrderByBuilder(getBuilder, touchVersion, ready) {
|
|
|
2796
3269
|
},
|
|
2797
3270
|
first: () => {
|
|
2798
3271
|
touchVersion();
|
|
2799
|
-
return ready.then(
|
|
3272
|
+
return ready.then(
|
|
3273
|
+
() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull))
|
|
3274
|
+
);
|
|
2800
3275
|
},
|
|
2801
3276
|
count: () => {
|
|
2802
3277
|
touchVersion();
|
|
@@ -2806,21 +3281,24 @@ function wrapOrderByBuilder(getBuilder, touchVersion, ready) {
|
|
|
2806
3281
|
}
|
|
2807
3282
|
|
|
2808
3283
|
// src/svelte/collection.svelte.ts
|
|
2809
|
-
|
|
3284
|
+
var Collection = class {
|
|
2810
3285
|
error = $state(null);
|
|
2811
3286
|
#handle = null;
|
|
2812
3287
|
#ready = createDeferred();
|
|
2813
3288
|
#version = $state(0);
|
|
2814
3289
|
#watchAbort = null;
|
|
2815
3290
|
#watchFiber = null;
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
3291
|
+
#logLevel = "None";
|
|
3292
|
+
/** @internal */
|
|
3293
|
+
_bind(handle, logLevel = "None") {
|
|
3294
|
+
if (this.#handle) return;
|
|
2819
3295
|
this.#handle = handle;
|
|
3296
|
+
this.#logLevel = logLevel;
|
|
2820
3297
|
this.error = null;
|
|
2821
3298
|
this.#settleReady();
|
|
2822
3299
|
this.#startWatch();
|
|
2823
3300
|
}
|
|
3301
|
+
/** @internal */
|
|
2824
3302
|
_fail(err) {
|
|
2825
3303
|
this.error = err;
|
|
2826
3304
|
this.#settleReady(err);
|
|
@@ -2833,31 +3311,41 @@ class Collection {
|
|
|
2833
3311
|
}
|
|
2834
3312
|
}
|
|
2835
3313
|
#startWatch() {
|
|
2836
|
-
if (!this.#handle)
|
|
2837
|
-
|
|
2838
|
-
const abort = new AbortController;
|
|
3314
|
+
if (!this.#handle) return;
|
|
3315
|
+
const abort = new AbortController();
|
|
2839
3316
|
this.#watchAbort = abort;
|
|
2840
|
-
this.#watchFiber = Effect24.runFork(
|
|
2841
|
-
|
|
2842
|
-
this.#
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
3317
|
+
this.#watchFiber = Effect24.runFork(
|
|
3318
|
+
Stream4.runForEach(
|
|
3319
|
+
this.#handle.watch(),
|
|
3320
|
+
(_records) => Effect24.sync(() => {
|
|
3321
|
+
if (!abort.signal.aborted) {
|
|
3322
|
+
this.#version++;
|
|
3323
|
+
}
|
|
3324
|
+
})
|
|
3325
|
+
).pipe(
|
|
3326
|
+
Effect24.catch(
|
|
3327
|
+
(e) => Effect24.sync(() => {
|
|
3328
|
+
if (!abort.signal.aborted) {
|
|
3329
|
+
this.error = e instanceof Error ? e : new Error(String(e));
|
|
3330
|
+
}
|
|
3331
|
+
})
|
|
3332
|
+
),
|
|
3333
|
+
Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)
|
|
3334
|
+
)
|
|
3335
|
+
);
|
|
2849
3336
|
}
|
|
2850
3337
|
#touchVersion = () => {
|
|
2851
|
-
this.#version;
|
|
3338
|
+
void this.#version;
|
|
2852
3339
|
};
|
|
2853
3340
|
#handleOrThrow = () => {
|
|
2854
|
-
if (this.#handle)
|
|
2855
|
-
return this.#handle;
|
|
3341
|
+
if (this.#handle) return this.#handle;
|
|
2856
3342
|
throw this.error ?? new ClosedError({ message: "Collection is not ready" });
|
|
2857
3343
|
};
|
|
2858
3344
|
#run = async (getEffect) => {
|
|
2859
3345
|
await this.#ready.promise;
|
|
2860
|
-
return Effect24.runPromise(
|
|
3346
|
+
return Effect24.runPromise(
|
|
3347
|
+
getEffect().pipe(Effect24.provideService(References5.MinimumLogLevel, this.#logLevel))
|
|
3348
|
+
);
|
|
2861
3349
|
};
|
|
2862
3350
|
add = (data) => {
|
|
2863
3351
|
return this.#run(() => this.#handleOrThrow().add(data));
|
|
@@ -2876,7 +3364,11 @@ class Collection {
|
|
|
2876
3364
|
return this.#run(() => this.#handleOrThrow().get(id));
|
|
2877
3365
|
}
|
|
2878
3366
|
this.#touchVersion();
|
|
2879
|
-
return this.#run(
|
|
3367
|
+
return this.#run(
|
|
3368
|
+
() => Stream4.runHead(this.#handleOrThrow().watch()).pipe(
|
|
3369
|
+
Effect24.map((opt) => Option11.getOrElse(opt, () => []))
|
|
3370
|
+
)
|
|
3371
|
+
);
|
|
2880
3372
|
}
|
|
2881
3373
|
first = () => {
|
|
2882
3374
|
this.#touchVersion();
|
|
@@ -2887,11 +3379,20 @@ class Collection {
|
|
|
2887
3379
|
return this.#run(() => this.#handleOrThrow().count());
|
|
2888
3380
|
};
|
|
2889
3381
|
where = (field2) => {
|
|
2890
|
-
return wrapWhereClause(
|
|
3382
|
+
return wrapWhereClause(
|
|
3383
|
+
() => this.#handleOrThrow().where(field2),
|
|
3384
|
+
this.#touchVersion,
|
|
3385
|
+
this.#ready.promise
|
|
3386
|
+
);
|
|
2891
3387
|
};
|
|
2892
3388
|
orderBy = (field2) => {
|
|
2893
|
-
return wrapOrderByBuilder(
|
|
3389
|
+
return wrapOrderByBuilder(
|
|
3390
|
+
() => this.#handleOrThrow().orderBy(field2),
|
|
3391
|
+
this.#touchVersion,
|
|
3392
|
+
this.#ready.promise
|
|
3393
|
+
);
|
|
2894
3394
|
};
|
|
3395
|
+
/** @internal */
|
|
2895
3396
|
_destroy(reason = new ClosedError({ message: "Collection is closed" })) {
|
|
2896
3397
|
if (this.#watchAbort) {
|
|
2897
3398
|
this.#watchAbort.abort();
|
|
@@ -2905,10 +3406,10 @@ class Collection {
|
|
|
2905
3406
|
this.error ??= reason;
|
|
2906
3407
|
this.#settleReady(this.error);
|
|
2907
3408
|
}
|
|
2908
|
-
}
|
|
3409
|
+
};
|
|
2909
3410
|
|
|
2910
3411
|
// src/svelte/tablinum.svelte.ts
|
|
2911
|
-
|
|
3412
|
+
var Tablinum2 = class {
|
|
2912
3413
|
status = $state("initializing");
|
|
2913
3414
|
syncStatus = $state("idle");
|
|
2914
3415
|
pendingCount = $state(0);
|
|
@@ -2917,15 +3418,17 @@ class Tablinum2 {
|
|
|
2917
3418
|
ready;
|
|
2918
3419
|
#handle = null;
|
|
2919
3420
|
#scope = null;
|
|
2920
|
-
#collections = new Map;
|
|
2921
|
-
#members = new Collection;
|
|
3421
|
+
#collections = /* @__PURE__ */ new Map();
|
|
3422
|
+
#members = new Collection();
|
|
2922
3423
|
#unsubscribeSyncStatus = null;
|
|
2923
3424
|
#unsubscribePendingCount = null;
|
|
2924
3425
|
#unsubscribeRelayStatus = null;
|
|
2925
3426
|
#closed = false;
|
|
2926
3427
|
#readyState = createDeferred();
|
|
3428
|
+
#logLevel;
|
|
2927
3429
|
constructor(config) {
|
|
2928
3430
|
this.ready = this.#readyState.promise;
|
|
3431
|
+
this.#logLevel = resolveLogLevel(config.logLevel);
|
|
2929
3432
|
this.#init(config);
|
|
2930
3433
|
}
|
|
2931
3434
|
#settleReady(err) {
|
|
@@ -2936,25 +3439,29 @@ class Tablinum2 {
|
|
|
2936
3439
|
}
|
|
2937
3440
|
}
|
|
2938
3441
|
#bindCollections(handle) {
|
|
2939
|
-
this.#members._bind(handle.members);
|
|
3442
|
+
this.#members._bind(handle.members, this.#logLevel);
|
|
2940
3443
|
for (const [name, collection2] of this.#collections) {
|
|
2941
|
-
collection2._bind(handle.collection(name));
|
|
3444
|
+
collection2._bind(handle.collection(name), this.#logLevel);
|
|
2942
3445
|
}
|
|
2943
3446
|
}
|
|
2944
3447
|
#runHandleEffect = async (run) => {
|
|
2945
3448
|
const handle = this.#requireReady();
|
|
2946
3449
|
try {
|
|
2947
3450
|
this.error = null;
|
|
2948
|
-
return await Effect25.runPromise(
|
|
3451
|
+
return await Effect25.runPromise(
|
|
3452
|
+
run(handle).pipe(Effect25.provideService(References6.MinimumLogLevel, this.#logLevel))
|
|
3453
|
+
);
|
|
2949
3454
|
} catch (e) {
|
|
2950
3455
|
this.error = e instanceof Error ? e : new Error(String(e));
|
|
2951
3456
|
throw this.error;
|
|
2952
3457
|
}
|
|
2953
3458
|
};
|
|
2954
|
-
async#init(config) {
|
|
3459
|
+
async #init(config) {
|
|
2955
3460
|
const scope = Effect25.runSync(Scope6.make());
|
|
2956
3461
|
try {
|
|
2957
|
-
const handle = await Effect25.runPromise(
|
|
3462
|
+
const handle = await Effect25.runPromise(
|
|
3463
|
+
createTablinum(config).pipe(Effect25.provideService(Scope6.Scope, scope))
|
|
3464
|
+
);
|
|
2958
3465
|
if (this.#closed) {
|
|
2959
3466
|
await Effect25.runPromise(Scope6.close(scope, Exit2.void));
|
|
2960
3467
|
this.#scope = null;
|
|
@@ -2977,7 +3484,8 @@ class Tablinum2 {
|
|
|
2977
3484
|
this.status = "ready";
|
|
2978
3485
|
this.#settleReady();
|
|
2979
3486
|
} catch (e) {
|
|
2980
|
-
await Effect25.runPromise(Scope6.close(scope, Exit2.fail(e))).catch(() => {
|
|
3487
|
+
await Effect25.runPromise(Scope6.close(scope, Exit2.fail(e))).catch(() => {
|
|
3488
|
+
});
|
|
2981
3489
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
2982
3490
|
this.error = err;
|
|
2983
3491
|
this.status = "error";
|
|
@@ -3000,10 +3508,10 @@ class Tablinum2 {
|
|
|
3000
3508
|
}
|
|
3001
3509
|
let col = this.#collections.get(name);
|
|
3002
3510
|
if (!col) {
|
|
3003
|
-
col = new Collection;
|
|
3511
|
+
col = new Collection();
|
|
3004
3512
|
this.#collections.set(name, col);
|
|
3005
3513
|
if (this.#handle) {
|
|
3006
|
-
col._bind(this.#handle.collection(name));
|
|
3514
|
+
col._bind(this.#handle.collection(name), this.#logLevel);
|
|
3007
3515
|
}
|
|
3008
3516
|
}
|
|
3009
3517
|
return col;
|
|
@@ -3021,8 +3529,7 @@ class Tablinum2 {
|
|
|
3021
3529
|
return this.#requireReady().exportInvite();
|
|
3022
3530
|
}
|
|
3023
3531
|
close = async () => {
|
|
3024
|
-
if (this.#closed)
|
|
3025
|
-
return;
|
|
3532
|
+
if (this.#closed) return;
|
|
3026
3533
|
this.#closed = true;
|
|
3027
3534
|
const closeError = new ClosedError({
|
|
3028
3535
|
message: this.#readyState.settled() ? "Tablinum is closed" : "Tablinum was closed before initialization completed"
|
|
@@ -3040,8 +3547,7 @@ class Tablinum2 {
|
|
|
3040
3547
|
this.#unsubscribeRelayStatus = null;
|
|
3041
3548
|
}
|
|
3042
3549
|
this.#members._destroy(closeError);
|
|
3043
|
-
for (const col of this.#collections.values())
|
|
3044
|
-
col._destroy(closeError);
|
|
3550
|
+
for (const col of this.#collections.values()) col._destroy(closeError);
|
|
3045
3551
|
this.#collections.clear();
|
|
3046
3552
|
const handle = this.#handle;
|
|
3047
3553
|
const scope = this.#scope;
|
|
@@ -3064,19 +3570,19 @@ class Tablinum2 {
|
|
|
3064
3570
|
getMembers = async () => this.#runHandleEffect((handle) => handle.getMembers());
|
|
3065
3571
|
getProfile = async () => this.#runHandleEffect((handle) => handle.getProfile());
|
|
3066
3572
|
setProfile = async (profile) => this.#runHandleEffect((handle) => handle.setProfile(profile));
|
|
3067
|
-
}
|
|
3573
|
+
};
|
|
3068
3574
|
export {
|
|
3069
|
-
|
|
3070
|
-
encodeInvite,
|
|
3071
|
-
decodeInvite,
|
|
3072
|
-
collection,
|
|
3073
|
-
ValidationError,
|
|
3074
|
-
Tablinum2 as Tablinum,
|
|
3075
|
-
SyncError,
|
|
3076
|
-
StorageError,
|
|
3077
|
-
RelayError,
|
|
3078
|
-
NotFoundError,
|
|
3079
|
-
CryptoError,
|
|
3575
|
+
ClosedError,
|
|
3080
3576
|
Collection,
|
|
3081
|
-
|
|
3577
|
+
CryptoError,
|
|
3578
|
+
NotFoundError,
|
|
3579
|
+
RelayError,
|
|
3580
|
+
StorageError,
|
|
3581
|
+
SyncError,
|
|
3582
|
+
Tablinum2 as Tablinum,
|
|
3583
|
+
ValidationError,
|
|
3584
|
+
collection,
|
|
3585
|
+
decodeInvite,
|
|
3586
|
+
encodeInvite,
|
|
3587
|
+
field
|
|
3082
3588
|
};
|