tablinum 0.5.0 → 0.6.1
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/index.js +1392 -937
- package/dist/storage/idb.d.ts +0 -1
- package/dist/svelte/index.svelte.js +1461 -975
- package/dist/sync/compact-event.d.ts +3 -0
- package/dist/sync/gift-wrap.d.ts +1 -2
- package/dist/sync/sync-service.d.ts +2 -0
- package/package.json +8 -27
- package/README.md +0 -298
|
@@ -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,29 +212,24 @@ 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
234
|
import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
|
|
221
235
|
|
|
@@ -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
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
|
});
|
|
@@ -436,15 +459,22 @@ function buildStructSchema(def, options = {}) {
|
|
|
436
459
|
}
|
|
437
460
|
for (const [name, fieldDef] of Object.entries(def.fields)) {
|
|
438
461
|
const fieldSchema = fieldDefToSchema(fieldDef);
|
|
439
|
-
schemaFields[name] = options.allOptional ? Schema4.optionalKey(fieldSchema) : fieldSchema;
|
|
462
|
+
schemaFields[name] = options.allOptional || fieldDef.isOptional ? Schema4.optionalKey(fieldSchema) : fieldSchema;
|
|
440
463
|
}
|
|
441
464
|
return Schema4.Struct(schemaFields);
|
|
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,15 +486,20 @@ 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
|
|
|
@@ -475,16 +510,14 @@ import { Effect as Effect6, Option as Option3, References } from "effect";
|
|
|
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
|
}
|
|
@@ -723,8 +797,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
723
797
|
const commitEvent = (event) => Effect6.gen(function* () {
|
|
724
798
|
yield* storage.putEvent(event);
|
|
725
799
|
yield* applyEvent(storage, event);
|
|
726
|
-
if (onWrite)
|
|
727
|
-
yield* onWrite(event);
|
|
800
|
+
if (onWrite) yield* onWrite(event);
|
|
728
801
|
yield* notifyChange(watchCtx, {
|
|
729
802
|
collection: collectionName,
|
|
730
803
|
recordId: event.recordId,
|
|
@@ -732,70 +805,84 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
732
805
|
});
|
|
733
806
|
});
|
|
734
807
|
const handle = {
|
|
735
|
-
add: (data) => withLog(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
recordId: id,
|
|
743
|
-
kind: "c",
|
|
744
|
-
data: fullRecord,
|
|
745
|
-
createdAt: Date.now(),
|
|
746
|
-
author: localAuthor
|
|
747
|
-
};
|
|
748
|
-
yield* commitEvent(event);
|
|
749
|
-
yield* Effect6.logDebug("Record added", { collection: collectionName, recordId: id, data: fullRecord });
|
|
750
|
-
return id;
|
|
751
|
-
})),
|
|
752
|
-
update: (id, data) => withLog(Effect6.gen(function* () {
|
|
753
|
-
const existing = yield* storage.getRecord(collectionName, id);
|
|
754
|
-
if (!existing || existing._d) {
|
|
755
|
-
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(),
|
|
756
815
|
collection: collectionName,
|
|
757
|
-
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
|
|
758
827
|
});
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
delete: (id) => withLog(Effect6.gen(function* () {
|
|
779
|
-
const existing = yield* storage.getRecord(collectionName, id);
|
|
780
|
-
if (!existing || existing._d) {
|
|
781
|
-
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(),
|
|
782
847
|
collection: collectionName,
|
|
783
|
-
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
|
|
784
859
|
});
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
+
),
|
|
799
886
|
undo: (id) => Effect6.gen(function* () {
|
|
800
887
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
801
888
|
if (!existing) {
|
|
@@ -836,15 +923,29 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
836
923
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
837
924
|
}),
|
|
838
925
|
count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
839
|
-
watch: () => watchCollection(watchCtx, storage, collectionName,
|
|
840
|
-
where: (fieldName) => createWhereClause(
|
|
841
|
-
|
|
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
|
+
)
|
|
842
943
|
};
|
|
843
944
|
return handle;
|
|
844
945
|
}
|
|
845
946
|
|
|
846
947
|
// src/sync/sync-service.ts
|
|
847
|
-
import { Effect as Effect8, Layer, Option as Option5, References as References2, 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";
|
|
848
949
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
849
950
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
850
951
|
|
|
@@ -860,8 +961,7 @@ var Mode = {
|
|
|
860
961
|
Fingerprint: 1,
|
|
861
962
|
IdList: 2
|
|
862
963
|
};
|
|
863
|
-
|
|
864
|
-
class WrappedBuffer {
|
|
964
|
+
var WrappedBuffer = class {
|
|
865
965
|
constructor(buffer) {
|
|
866
966
|
this._raw = new Uint8Array(buffer || 512);
|
|
867
967
|
this.length = buffer ? buffer.length : 0;
|
|
@@ -873,10 +973,8 @@ class WrappedBuffer {
|
|
|
873
973
|
return this._raw.byteLength;
|
|
874
974
|
}
|
|
875
975
|
extend(buf) {
|
|
876
|
-
if (buf._raw)
|
|
877
|
-
|
|
878
|
-
if (typeof buf.length !== "number")
|
|
879
|
-
throw Error("bad length");
|
|
976
|
+
if (buf._raw) buf = buf.unwrap();
|
|
977
|
+
if (typeof buf.length !== "number") throw Error("bad length");
|
|
880
978
|
const targetSize = buf.length + this.length;
|
|
881
979
|
if (this.capacity < targetSize) {
|
|
882
980
|
const oldRaw = this._raw;
|
|
@@ -899,42 +997,36 @@ class WrappedBuffer {
|
|
|
899
997
|
this.length -= n;
|
|
900
998
|
return firstSubarray;
|
|
901
999
|
}
|
|
902
|
-
}
|
|
1000
|
+
};
|
|
903
1001
|
function decodeVarInt(buf) {
|
|
904
1002
|
let res = 0;
|
|
905
|
-
while (
|
|
906
|
-
if (buf.length === 0)
|
|
907
|
-
throw Error("parse ends prematurely");
|
|
1003
|
+
while (1) {
|
|
1004
|
+
if (buf.length === 0) throw Error("parse ends prematurely");
|
|
908
1005
|
let byte = buf.shift();
|
|
909
1006
|
res = res << 7 | byte & 127;
|
|
910
|
-
if ((byte & 128) === 0)
|
|
911
|
-
break;
|
|
1007
|
+
if ((byte & 128) === 0) break;
|
|
912
1008
|
}
|
|
913
1009
|
return res;
|
|
914
1010
|
}
|
|
915
1011
|
function encodeVarInt(n) {
|
|
916
|
-
if (n === 0)
|
|
917
|
-
return new WrappedBuffer([0]);
|
|
1012
|
+
if (n === 0) return new WrappedBuffer([0]);
|
|
918
1013
|
let o = [];
|
|
919
1014
|
while (n !== 0) {
|
|
920
1015
|
o.push(n & 127);
|
|
921
1016
|
n >>>= 7;
|
|
922
1017
|
}
|
|
923
1018
|
o.reverse();
|
|
924
|
-
for (let i = 0;i < o.length - 1; i++)
|
|
925
|
-
o[i] |= 128;
|
|
1019
|
+
for (let i = 0; i < o.length - 1; i++) o[i] |= 128;
|
|
926
1020
|
return new WrappedBuffer(o);
|
|
927
1021
|
}
|
|
928
1022
|
function getByte(buf) {
|
|
929
1023
|
return getBytes(buf, 1)[0];
|
|
930
1024
|
}
|
|
931
1025
|
function getBytes(buf, n) {
|
|
932
|
-
if (buf.length < n)
|
|
933
|
-
throw Error("parse ends prematurely");
|
|
1026
|
+
if (buf.length < n) throw Error("parse ends prematurely");
|
|
934
1027
|
return buf.shiftN(n);
|
|
935
1028
|
}
|
|
936
|
-
|
|
937
|
-
class Accumulator {
|
|
1029
|
+
var Accumulator = class {
|
|
938
1030
|
constructor() {
|
|
939
1031
|
this.setToZero();
|
|
940
1032
|
if (typeof window === "undefined") {
|
|
@@ -951,15 +1043,14 @@ class Accumulator {
|
|
|
951
1043
|
let currCarry = 0, nextCarry = 0;
|
|
952
1044
|
let p = new DataView(this.buf.buffer);
|
|
953
1045
|
let po = new DataView(otherBuf.buffer);
|
|
954
|
-
for (let i = 0;i < 8; i++) {
|
|
1046
|
+
for (let i = 0; i < 8; i++) {
|
|
955
1047
|
let offset = i * 4;
|
|
956
1048
|
let orig = p.getUint32(offset, true);
|
|
957
1049
|
let otherV = po.getUint32(offset, true);
|
|
958
1050
|
let next = orig;
|
|
959
1051
|
next += currCarry;
|
|
960
1052
|
next += otherV;
|
|
961
|
-
if (next > 4294967295)
|
|
962
|
-
nextCarry = 1;
|
|
1053
|
+
if (next > 4294967295) nextCarry = 1;
|
|
963
1054
|
p.setUint32(offset, next & 4294967295, true);
|
|
964
1055
|
currCarry = nextCarry;
|
|
965
1056
|
nextCarry = 0;
|
|
@@ -967,7 +1058,7 @@ class Accumulator {
|
|
|
967
1058
|
}
|
|
968
1059
|
negate() {
|
|
969
1060
|
let p = new DataView(this.buf.buffer);
|
|
970
|
-
for (let i = 0;i < 8; i++) {
|
|
1061
|
+
for (let i = 0; i < 8; i++) {
|
|
971
1062
|
let offset = i * 4;
|
|
972
1063
|
p.setUint32(offset, ~p.getUint32(offset, true));
|
|
973
1064
|
}
|
|
@@ -976,33 +1067,29 @@ class Accumulator {
|
|
|
976
1067
|
this.add(one);
|
|
977
1068
|
}
|
|
978
1069
|
async getFingerprint(n) {
|
|
979
|
-
let input = new WrappedBuffer;
|
|
1070
|
+
let input = new WrappedBuffer();
|
|
980
1071
|
input.extend(this.buf);
|
|
981
1072
|
input.extend(encodeVarInt(n));
|
|
982
1073
|
let hash = await this.sha256(input.unwrap());
|
|
983
1074
|
return hash.subarray(0, FINGERPRINT_SIZE);
|
|
984
1075
|
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
class NegentropyStorageVector {
|
|
1076
|
+
};
|
|
1077
|
+
var NegentropyStorageVector = class {
|
|
988
1078
|
constructor() {
|
|
989
1079
|
this.items = [];
|
|
990
1080
|
this.sealed = false;
|
|
991
1081
|
}
|
|
992
1082
|
insert(timestamp, id) {
|
|
993
|
-
if (this.sealed)
|
|
994
|
-
throw Error("already sealed");
|
|
1083
|
+
if (this.sealed) throw Error("already sealed");
|
|
995
1084
|
id = loadInputBuffer(id);
|
|
996
|
-
if (id.byteLength !== ID_SIZE)
|
|
997
|
-
throw Error("bad id size for added item");
|
|
1085
|
+
if (id.byteLength !== ID_SIZE) throw Error("bad id size for added item");
|
|
998
1086
|
this.items.push({ timestamp, id });
|
|
999
1087
|
}
|
|
1000
1088
|
seal() {
|
|
1001
|
-
if (this.sealed)
|
|
1002
|
-
throw Error("already sealed");
|
|
1089
|
+
if (this.sealed) throw Error("already sealed");
|
|
1003
1090
|
this.sealed = true;
|
|
1004
1091
|
this.items.sort(itemCompare);
|
|
1005
|
-
for (let i = 1;i < this.items.length; i++) {
|
|
1092
|
+
for (let i = 1; i < this.items.length; i++) {
|
|
1006
1093
|
if (itemCompare(this.items[i - 1], this.items[i]) === 0)
|
|
1007
1094
|
throw Error("duplicate item inserted");
|
|
1008
1095
|
}
|
|
@@ -1016,16 +1103,14 @@ class NegentropyStorageVector {
|
|
|
1016
1103
|
}
|
|
1017
1104
|
getItem(i) {
|
|
1018
1105
|
this._checkSealed();
|
|
1019
|
-
if (i >= this.items.length)
|
|
1020
|
-
throw Error("out of range");
|
|
1106
|
+
if (i >= this.items.length) throw Error("out of range");
|
|
1021
1107
|
return this.items[i];
|
|
1022
1108
|
}
|
|
1023
1109
|
iterate(begin, end, cb) {
|
|
1024
1110
|
this._checkSealed();
|
|
1025
1111
|
this._checkBounds(begin, end);
|
|
1026
|
-
for (let i = begin;i < end; ++i) {
|
|
1027
|
-
if (!cb(this.items[i], i))
|
|
1028
|
-
break;
|
|
1112
|
+
for (let i = begin; i < end; ++i) {
|
|
1113
|
+
if (!cb(this.items[i], i)) break;
|
|
1029
1114
|
}
|
|
1030
1115
|
}
|
|
1031
1116
|
findLowerBound(begin, end, bound) {
|
|
@@ -1034,7 +1119,7 @@ class NegentropyStorageVector {
|
|
|
1034
1119
|
return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0);
|
|
1035
1120
|
}
|
|
1036
1121
|
async fingerprint(begin, end) {
|
|
1037
|
-
let out = new Accumulator;
|
|
1122
|
+
let out = new Accumulator();
|
|
1038
1123
|
out.setToZero();
|
|
1039
1124
|
this.iterate(begin, end, (item, i) => {
|
|
1040
1125
|
out.add(item.id);
|
|
@@ -1043,12 +1128,10 @@ class NegentropyStorageVector {
|
|
|
1043
1128
|
return await out.getFingerprint(end - begin);
|
|
1044
1129
|
}
|
|
1045
1130
|
_checkSealed() {
|
|
1046
|
-
if (!this.sealed)
|
|
1047
|
-
throw Error("not sealed");
|
|
1131
|
+
if (!this.sealed) throw Error("not sealed");
|
|
1048
1132
|
}
|
|
1049
1133
|
_checkBounds(begin, end) {
|
|
1050
|
-
if (begin > end || end > this.items.length)
|
|
1051
|
-
throw Error("bad range");
|
|
1134
|
+
if (begin > end || end > this.items.length) throw Error("bad range");
|
|
1052
1135
|
}
|
|
1053
1136
|
_binarySearch(arr, first, last, cmp) {
|
|
1054
1137
|
let count = last - first;
|
|
@@ -1065,12 +1148,10 @@ class NegentropyStorageVector {
|
|
|
1065
1148
|
}
|
|
1066
1149
|
return first;
|
|
1067
1150
|
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
class Negentropy {
|
|
1151
|
+
};
|
|
1152
|
+
var Negentropy = class {
|
|
1071
1153
|
constructor(storage, frameSizeLimit = 0) {
|
|
1072
|
-
if (frameSizeLimit !== 0 && frameSizeLimit < 4096)
|
|
1073
|
-
throw Error("frameSizeLimit too small");
|
|
1154
|
+
if (frameSizeLimit !== 0 && frameSizeLimit < 4096) throw Error("frameSizeLimit too small");
|
|
1074
1155
|
this.storage = storage;
|
|
1075
1156
|
this.frameSizeLimit = frameSizeLimit;
|
|
1076
1157
|
this.lastTimestampIn = 0;
|
|
@@ -1080,10 +1161,9 @@ class Negentropy {
|
|
|
1080
1161
|
return { timestamp, id: id ? id : new Uint8Array(0) };
|
|
1081
1162
|
}
|
|
1082
1163
|
async initiate() {
|
|
1083
|
-
if (this.isInitiator)
|
|
1084
|
-
throw Error("already initiated");
|
|
1164
|
+
if (this.isInitiator) throw Error("already initiated");
|
|
1085
1165
|
this.isInitiator = true;
|
|
1086
|
-
let output = new WrappedBuffer;
|
|
1166
|
+
let output = new WrappedBuffer();
|
|
1087
1167
|
output.extend([PROTOCOL_VERSION]);
|
|
1088
1168
|
await this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output);
|
|
1089
1169
|
return this._renderOutput(output);
|
|
@@ -1095,23 +1175,24 @@ class Negentropy {
|
|
|
1095
1175
|
let haveIds = [], needIds = [];
|
|
1096
1176
|
query = new WrappedBuffer(loadInputBuffer(query));
|
|
1097
1177
|
this.lastTimestampIn = this.lastTimestampOut = 0;
|
|
1098
|
-
let fullOutput = new WrappedBuffer;
|
|
1178
|
+
let fullOutput = new WrappedBuffer();
|
|
1099
1179
|
fullOutput.extend([PROTOCOL_VERSION]);
|
|
1100
1180
|
let protocolVersion = getByte(query);
|
|
1101
1181
|
if (protocolVersion < 96 || protocolVersion > 111)
|
|
1102
1182
|
throw Error("invalid negentropy protocol version byte");
|
|
1103
1183
|
if (protocolVersion !== PROTOCOL_VERSION) {
|
|
1104
1184
|
if (this.isInitiator)
|
|
1105
|
-
throw Error(
|
|
1106
|
-
|
|
1107
|
-
|
|
1185
|
+
throw Error(
|
|
1186
|
+
"unsupported negentropy protocol version requested: " + (protocolVersion - 96)
|
|
1187
|
+
);
|
|
1188
|
+
else return [this._renderOutput(fullOutput), haveIds, needIds];
|
|
1108
1189
|
}
|
|
1109
1190
|
let storageSize = this.storage.size();
|
|
1110
1191
|
let prevBound = this._bound(0);
|
|
1111
1192
|
let prevIndex = 0;
|
|
1112
1193
|
let skip = false;
|
|
1113
1194
|
while (query.length !== 0) {
|
|
1114
|
-
let o = new WrappedBuffer;
|
|
1195
|
+
let o = new WrappedBuffer();
|
|
1115
1196
|
let doSkip = () => {
|
|
1116
1197
|
if (skip) {
|
|
1117
1198
|
skip = false;
|
|
@@ -1137,10 +1218,9 @@ class Negentropy {
|
|
|
1137
1218
|
} else if (mode === Mode.IdList) {
|
|
1138
1219
|
let numIds = decodeVarInt(query);
|
|
1139
1220
|
let theirElems = {};
|
|
1140
|
-
for (let i = 0;i < numIds; i++) {
|
|
1221
|
+
for (let i = 0; i < numIds; i++) {
|
|
1141
1222
|
let e = getBytes(query, ID_SIZE);
|
|
1142
|
-
if (this.isInitiator)
|
|
1143
|
-
theirElems[e] = e;
|
|
1223
|
+
if (this.isInitiator) theirElems[e] = e;
|
|
1144
1224
|
}
|
|
1145
1225
|
if (this.isInitiator) {
|
|
1146
1226
|
skip = true;
|
|
@@ -1159,7 +1239,7 @@ class Negentropy {
|
|
|
1159
1239
|
}
|
|
1160
1240
|
} else {
|
|
1161
1241
|
doSkip();
|
|
1162
|
-
let responseIds = new WrappedBuffer;
|
|
1242
|
+
let responseIds = new WrappedBuffer();
|
|
1163
1243
|
let numResponseIds = 0;
|
|
1164
1244
|
let endBound = currBound;
|
|
1165
1245
|
this.storage.iterate(lower, upper, (item, index) => {
|
|
@@ -1177,7 +1257,7 @@ class Negentropy {
|
|
|
1177
1257
|
o.extend(encodeVarInt(numResponseIds));
|
|
1178
1258
|
o.extend(responseIds);
|
|
1179
1259
|
fullOutput.extend(o);
|
|
1180
|
-
o = new WrappedBuffer;
|
|
1260
|
+
o = new WrappedBuffer();
|
|
1181
1261
|
}
|
|
1182
1262
|
} else {
|
|
1183
1263
|
throw Error("unexpected mode");
|
|
@@ -1215,7 +1295,7 @@ class Negentropy {
|
|
|
1215
1295
|
let itemsPerBucket = Math.floor(numElems / buckets);
|
|
1216
1296
|
let bucketsWithExtra = numElems % buckets;
|
|
1217
1297
|
let curr = lower;
|
|
1218
|
-
for (let i = 0;i < buckets; i++) {
|
|
1298
|
+
for (let i = 0; i < buckets; i++) {
|
|
1219
1299
|
let bucketSize = itemsPerBucket + (i < bucketsWithExtra ? 1 : 0);
|
|
1220
1300
|
let ourFingerprint = await this.storage.fingerprint(curr, curr + bucketSize);
|
|
1221
1301
|
curr += bucketSize;
|
|
@@ -1225,10 +1305,8 @@ class Negentropy {
|
|
|
1225
1305
|
} else {
|
|
1226
1306
|
let prevItem, currItem;
|
|
1227
1307
|
this.storage.iterate(curr - 1, curr + 1, (item, index) => {
|
|
1228
|
-
if (index === curr - 1)
|
|
1229
|
-
|
|
1230
|
-
else
|
|
1231
|
-
currItem = item;
|
|
1308
|
+
if (index === curr - 1) prevItem = item;
|
|
1309
|
+
else currItem = item;
|
|
1232
1310
|
return true;
|
|
1233
1311
|
});
|
|
1234
1312
|
nextBound = this.getMinimalBound(prevItem, currItem);
|
|
@@ -1241,13 +1319,13 @@ class Negentropy {
|
|
|
1241
1319
|
}
|
|
1242
1320
|
_renderOutput(o) {
|
|
1243
1321
|
o = o.unwrap();
|
|
1244
|
-
if (!this.wantUint8ArrayOutput)
|
|
1245
|
-
o = uint8ArrayToHex(o);
|
|
1322
|
+
if (!this.wantUint8ArrayOutput) o = uint8ArrayToHex(o);
|
|
1246
1323
|
return o;
|
|
1247
1324
|
}
|
|
1248
1325
|
exceededFrameSizeLimit(n) {
|
|
1249
1326
|
return this.frameSizeLimit && n > this.frameSizeLimit - 200;
|
|
1250
1327
|
}
|
|
1328
|
+
// Decoding
|
|
1251
1329
|
decodeTimestampIn(encoded) {
|
|
1252
1330
|
let timestamp = decodeVarInt(encoded);
|
|
1253
1331
|
timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1;
|
|
@@ -1262,11 +1340,11 @@ class Negentropy {
|
|
|
1262
1340
|
decodeBound(encoded) {
|
|
1263
1341
|
let timestamp = this.decodeTimestampIn(encoded);
|
|
1264
1342
|
let len = decodeVarInt(encoded);
|
|
1265
|
-
if (len > ID_SIZE)
|
|
1266
|
-
throw Error("bound key too long");
|
|
1343
|
+
if (len > ID_SIZE) throw Error("bound key too long");
|
|
1267
1344
|
let id = getBytes(encoded, len);
|
|
1268
1345
|
return { timestamp, id };
|
|
1269
1346
|
}
|
|
1347
|
+
// Encoding
|
|
1270
1348
|
encodeTimestampOut(timestamp) {
|
|
1271
1349
|
if (timestamp === Number.MAX_VALUE) {
|
|
1272
1350
|
this.lastTimestampOut = Number.MAX_VALUE;
|
|
@@ -1278,7 +1356,7 @@ class Negentropy {
|
|
|
1278
1356
|
return encodeVarInt(timestamp + 1);
|
|
1279
1357
|
}
|
|
1280
1358
|
encodeBound(key) {
|
|
1281
|
-
let output = new WrappedBuffer;
|
|
1359
|
+
let output = new WrappedBuffer();
|
|
1282
1360
|
output.extend(this.encodeTimestampOut(key.timestamp));
|
|
1283
1361
|
output.extend(encodeVarInt(key.id.length));
|
|
1284
1362
|
output.extend(key.id);
|
|
@@ -1291,30 +1369,24 @@ class Negentropy {
|
|
|
1291
1369
|
let sharedPrefixBytes = 0;
|
|
1292
1370
|
let currKey = curr.id;
|
|
1293
1371
|
let prevKey = prev.id;
|
|
1294
|
-
for (let i = 0;i < ID_SIZE; i++) {
|
|
1295
|
-
if (currKey[i] !== prevKey[i])
|
|
1296
|
-
break;
|
|
1372
|
+
for (let i = 0; i < ID_SIZE; i++) {
|
|
1373
|
+
if (currKey[i] !== prevKey[i]) break;
|
|
1297
1374
|
sharedPrefixBytes++;
|
|
1298
1375
|
}
|
|
1299
1376
|
return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1));
|
|
1300
1377
|
}
|
|
1301
1378
|
}
|
|
1302
|
-
}
|
|
1379
|
+
};
|
|
1303
1380
|
function loadInputBuffer(inp) {
|
|
1304
|
-
if (typeof inp === "string")
|
|
1305
|
-
|
|
1306
|
-
else if (__proto__ !== Uint8Array.prototype)
|
|
1307
|
-
inp = new Uint8Array(inp);
|
|
1381
|
+
if (typeof inp === "string") inp = hexToUint8Array(inp);
|
|
1382
|
+
else if (__proto__ !== Uint8Array.prototype) inp = new Uint8Array(inp);
|
|
1308
1383
|
return inp;
|
|
1309
1384
|
}
|
|
1310
1385
|
function hexToUint8Array(h) {
|
|
1311
|
-
if (h.startsWith("0x"))
|
|
1312
|
-
|
|
1313
|
-
if (h.length % 2 === 1)
|
|
1314
|
-
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");
|
|
1315
1388
|
let arr = new Uint8Array(h.length / 2);
|
|
1316
|
-
for (let i = 0;i < arr.length; i++)
|
|
1317
|
-
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);
|
|
1318
1390
|
return arr;
|
|
1319
1391
|
}
|
|
1320
1392
|
var uint8ArrayToHexLookupTable = new Array(256);
|
|
@@ -1337,28 +1409,24 @@ var uint8ArrayToHexLookupTable = new Array(256);
|
|
|
1337
1409
|
"e",
|
|
1338
1410
|
"f"
|
|
1339
1411
|
];
|
|
1340
|
-
for (let i = 0;i < 256; i++) {
|
|
1412
|
+
for (let i = 0; i < 256; i++) {
|
|
1341
1413
|
uint8ArrayToHexLookupTable[i] = hexAlphabet[i >>> 4 & 15] + hexAlphabet[i & 15];
|
|
1342
1414
|
}
|
|
1343
1415
|
}
|
|
1344
1416
|
function uint8ArrayToHex(arr) {
|
|
1345
1417
|
let out = "";
|
|
1346
|
-
for (let i = 0, edx = arr.length;i < edx; i++) {
|
|
1418
|
+
for (let i = 0, edx = arr.length; i < edx; i++) {
|
|
1347
1419
|
out += uint8ArrayToHexLookupTable[arr[i]];
|
|
1348
1420
|
}
|
|
1349
1421
|
return out;
|
|
1350
1422
|
}
|
|
1351
1423
|
function compareUint8Array(a, b) {
|
|
1352
|
-
for (let i = 0;i < a.byteLength; i++) {
|
|
1353
|
-
if (a[i] < b[i])
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
if (a.byteLength > b.byteLength)
|
|
1359
|
-
return 1;
|
|
1360
|
-
if (a.byteLength < b.byteLength)
|
|
1361
|
-
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;
|
|
1362
1430
|
return 0;
|
|
1363
1431
|
}
|
|
1364
1432
|
function itemCompare(a, b) {
|
|
@@ -1374,7 +1442,7 @@ import { GiftWrap } from "nostr-tools/kinds";
|
|
|
1374
1442
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1375
1443
|
return Effect7.gen(function* () {
|
|
1376
1444
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1377
|
-
const storageVector = new NegentropyStorageVector;
|
|
1445
|
+
const storageVector = new NegentropyStorageVector();
|
|
1378
1446
|
for (const gw of allGiftWraps) {
|
|
1379
1447
|
storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
|
|
1380
1448
|
}
|
|
@@ -1387,7 +1455,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1387
1455
|
const allHaveIds = [];
|
|
1388
1456
|
const allNeedIds = [];
|
|
1389
1457
|
const subId = `neg-${Date.now()}`;
|
|
1390
|
-
const initialMsg = yield* Effect7.
|
|
1458
|
+
const initialMsg = yield* Effect7.tryPromise({
|
|
1391
1459
|
try: () => neg.initiate(),
|
|
1392
1460
|
catch: (e) => new SyncError({
|
|
1393
1461
|
message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1398,9 +1466,8 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1398
1466
|
let currentMsg = initialMsg;
|
|
1399
1467
|
while (currentMsg !== null) {
|
|
1400
1468
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1401
|
-
if (response.msgHex === null)
|
|
1402
|
-
|
|
1403
|
-
const reconcileResult = yield* Effect7.try({
|
|
1469
|
+
if (response.msgHex === null) break;
|
|
1470
|
+
const reconcileResult = yield* Effect7.tryPromise({
|
|
1404
1471
|
try: () => neg.reconcile(response.msgHex),
|
|
1405
1472
|
catch: (e) => new SyncError({
|
|
1406
1473
|
message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1409,10 +1476,8 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1409
1476
|
})
|
|
1410
1477
|
});
|
|
1411
1478
|
const [nextMsg, haveIds, needIds] = reconcileResult;
|
|
1412
|
-
for (const id of haveIds)
|
|
1413
|
-
|
|
1414
|
-
for (const id of needIds)
|
|
1415
|
-
allNeedIds.push(id);
|
|
1479
|
+
for (const id of haveIds) allHaveIds.push(id);
|
|
1480
|
+
for (const id of needIds) allNeedIds.push(id);
|
|
1416
1481
|
currentMsg = nextMsg;
|
|
1417
1482
|
}
|
|
1418
1483
|
yield* Effect7.logDebug("Negentropy reconciliation complete", {
|
|
@@ -1460,12 +1525,11 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1460
1525
|
kind: 1,
|
|
1461
1526
|
content: JSON.stringify(rotationData),
|
|
1462
1527
|
tags: [["d", `_system:rotation:${epochId}`]],
|
|
1463
|
-
created_at: Math.floor(Date.now() /
|
|
1528
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1464
1529
|
};
|
|
1465
1530
|
const wrappedEvents = [];
|
|
1466
1531
|
for (const memberPubkey of remainingMemberPubkeys) {
|
|
1467
|
-
if (memberPubkey === senderPublicKey)
|
|
1468
|
-
continue;
|
|
1532
|
+
if (memberPubkey === senderPublicKey) continue;
|
|
1469
1533
|
const wrapped = wrapEvent(rumor, senderPrivateKey, memberPubkey);
|
|
1470
1534
|
wrappedEvents.push(wrapped);
|
|
1471
1535
|
}
|
|
@@ -1478,7 +1542,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1478
1542
|
kind: 1,
|
|
1479
1543
|
content: JSON.stringify(removalData),
|
|
1480
1544
|
tags: [["d", `_system:removed:${epochId}`]],
|
|
1481
|
-
created_at: Math.floor(Date.now() /
|
|
1545
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1482
1546
|
};
|
|
1483
1547
|
const removalNotices = [];
|
|
1484
1548
|
for (const removedPubkey of removedMemberPubkeys) {
|
|
@@ -1488,8 +1552,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1488
1552
|
return { epoch, wrappedEvents, removalNotices };
|
|
1489
1553
|
}
|
|
1490
1554
|
function parseRotationEvent(content, dTag) {
|
|
1491
|
-
if (!dTag.startsWith("_system:rotation:"))
|
|
1492
|
-
return Option4.none();
|
|
1555
|
+
if (!dTag.startsWith("_system:rotation:")) return Option4.none();
|
|
1493
1556
|
try {
|
|
1494
1557
|
return Option4.some(decodeRotationData(content));
|
|
1495
1558
|
} catch {
|
|
@@ -1497,8 +1560,7 @@ function parseRotationEvent(content, dTag) {
|
|
|
1497
1560
|
}
|
|
1498
1561
|
}
|
|
1499
1562
|
function parseRemovalNotice(content, dTag) {
|
|
1500
|
-
if (!dTag.startsWith("_system:removed:"))
|
|
1501
|
-
return Option4.none();
|
|
1563
|
+
if (!dTag.startsWith("_system:removed:")) return Option4.none();
|
|
1502
1564
|
try {
|
|
1503
1565
|
return Option4.some(decodeRemovalNotice(content));
|
|
1504
1566
|
} catch {
|
|
@@ -1518,99 +1580,124 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1518
1580
|
kind: "create"
|
|
1519
1581
|
});
|
|
1520
1582
|
const forkHandled = (effect) => {
|
|
1521
|
-
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
|
+
);
|
|
1522
1591
|
};
|
|
1523
1592
|
let autoFlushActive = false;
|
|
1524
1593
|
const autoFlushEffect = Effect8.gen(function* () {
|
|
1525
1594
|
const size = yield* publishQueue.size();
|
|
1526
|
-
if (size === 0)
|
|
1527
|
-
return;
|
|
1595
|
+
if (size === 0) return;
|
|
1528
1596
|
yield* syncStatus.set("syncing");
|
|
1529
1597
|
yield* publishQueue.flush(relayUrls);
|
|
1530
1598
|
const remaining = yield* publishQueue.size();
|
|
1531
|
-
if (remaining > 0)
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
+
);
|
|
1534
1605
|
const scheduleAutoFlush = () => {
|
|
1535
|
-
if (autoFlushActive)
|
|
1536
|
-
return;
|
|
1606
|
+
if (autoFlushActive) return;
|
|
1537
1607
|
autoFlushActive = true;
|
|
1538
|
-
forkHandled(
|
|
1539
|
-
|
|
1540
|
-
|
|
1608
|
+
forkHandled(
|
|
1609
|
+
autoFlushEffect.pipe(
|
|
1610
|
+
Effect8.ensuring(
|
|
1611
|
+
Effect8.sync(() => {
|
|
1612
|
+
autoFlushActive = false;
|
|
1613
|
+
})
|
|
1614
|
+
)
|
|
1615
|
+
)
|
|
1616
|
+
);
|
|
1541
1617
|
};
|
|
1542
1618
|
const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
|
|
1543
1619
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1544
|
-
if (!memberRecord)
|
|
1545
|
-
return false;
|
|
1620
|
+
if (!memberRecord) return false;
|
|
1546
1621
|
return !!memberRecord.removedAt;
|
|
1547
1622
|
});
|
|
1548
|
-
const
|
|
1623
|
+
const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
1624
|
+
const unwrapGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1549
1625
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1550
|
-
if (existing)
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1626
|
+
if (existing) return null;
|
|
1627
|
+
const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
|
|
1628
|
+
Effect8.orElseSucceed(() => null)
|
|
1629
|
+
);
|
|
1630
|
+
if (!rumor) {
|
|
1631
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1555
1632
|
return null;
|
|
1556
1633
|
}
|
|
1557
|
-
const rumor = unwrapResult.success;
|
|
1558
1634
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1559
1635
|
if (!dTag) {
|
|
1560
|
-
yield*
|
|
1636
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1561
1637
|
return null;
|
|
1562
1638
|
}
|
|
1563
1639
|
const colonIdx = dTag.indexOf(":");
|
|
1564
1640
|
if (colonIdx === -1) {
|
|
1565
|
-
yield*
|
|
1641
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1644
|
+
const collection2 = dTag.substring(0, colonIdx);
|
|
1645
|
+
if (!knownCollections.has(collection2)) {
|
|
1646
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1566
1647
|
return null;
|
|
1567
1648
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1649
|
+
return {
|
|
1650
|
+
giftWrap: remoteGw,
|
|
1651
|
+
rumor,
|
|
1652
|
+
collection: collection2,
|
|
1653
|
+
recordId: dTag.substring(colonIdx + 1)
|
|
1654
|
+
};
|
|
1655
|
+
});
|
|
1656
|
+
const applyUnwrappedEvent = (uw) => Effect8.gen(function* () {
|
|
1657
|
+
const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
|
|
1570
1658
|
const retention = knownCollections.get(collectionName);
|
|
1571
|
-
if (retention ===
|
|
1572
|
-
yield*
|
|
1659
|
+
if (retention === void 0) {
|
|
1660
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1573
1661
|
return null;
|
|
1574
1662
|
}
|
|
1575
1663
|
if (rumor.pubkey) {
|
|
1576
1664
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1577
1665
|
if (reject) {
|
|
1578
|
-
yield* Effect8.logWarning("Rejected write from removed member", {
|
|
1579
|
-
|
|
1666
|
+
yield* Effect8.logWarning("Rejected write from removed member", {
|
|
1667
|
+
author: rumor.pubkey.slice(0, 12)
|
|
1668
|
+
});
|
|
1669
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1580
1670
|
return null;
|
|
1581
1671
|
}
|
|
1582
1672
|
}
|
|
1583
|
-
let data = null;
|
|
1584
|
-
let kind = "u";
|
|
1585
1673
|
const parsed = yield* Effect8.try({
|
|
1586
1674
|
try: () => JSON.parse(rumor.content),
|
|
1587
|
-
catch: () =>
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
return;
|
|
1592
|
-
}));
|
|
1593
|
-
if (parsed === undefined) {
|
|
1594
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1675
|
+
catch: () => void 0
|
|
1676
|
+
}).pipe(Effect8.orElseSucceed(() => void 0));
|
|
1677
|
+
if (parsed === void 0) {
|
|
1678
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1595
1679
|
return null;
|
|
1596
1680
|
}
|
|
1681
|
+
let data = null;
|
|
1682
|
+
let kind = "u";
|
|
1597
1683
|
if (parsed === null || parsed._deleted) {
|
|
1598
1684
|
kind = "d";
|
|
1599
1685
|
} else {
|
|
1600
1686
|
data = parsed;
|
|
1601
1687
|
}
|
|
1602
|
-
const author = rumor.pubkey ||
|
|
1688
|
+
const author = rumor.pubkey || void 0;
|
|
1603
1689
|
const event = {
|
|
1604
1690
|
id: rumor.id,
|
|
1605
1691
|
collection: collectionName,
|
|
1606
1692
|
recordId,
|
|
1607
1693
|
kind,
|
|
1608
1694
|
data,
|
|
1609
|
-
createdAt: rumor.created_at *
|
|
1695
|
+
createdAt: rumor.created_at * 1e3,
|
|
1610
1696
|
author
|
|
1611
1697
|
};
|
|
1612
1698
|
yield* storage.putGiftWrap({
|
|
1613
1699
|
id: remoteGw.id,
|
|
1700
|
+
event: remoteGw,
|
|
1614
1701
|
createdAt: remoteGw.created_at
|
|
1615
1702
|
});
|
|
1616
1703
|
yield* storage.putEvent(event);
|
|
@@ -1618,12 +1705,89 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1618
1705
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1619
1706
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1620
1707
|
}
|
|
1621
|
-
yield* Effect8.logDebug("Processed gift wrap", {
|
|
1708
|
+
yield* Effect8.logDebug("Processed gift wrap", {
|
|
1709
|
+
collection: collectionName,
|
|
1710
|
+
recordId,
|
|
1711
|
+
kind,
|
|
1712
|
+
author: author?.slice(0, 12)
|
|
1713
|
+
});
|
|
1622
1714
|
if (author && onNewAuthor) {
|
|
1623
1715
|
onNewAuthor(author);
|
|
1624
1716
|
}
|
|
1625
1717
|
return collectionName;
|
|
1626
1718
|
});
|
|
1719
|
+
const reconcileRelay = (url, pubKeys) => Effect8.gen(function* () {
|
|
1720
|
+
yield* Effect8.logDebug("Syncing relay", { relay: url });
|
|
1721
|
+
const { haveIds, needIds } = yield* reconcileWithRelay(
|
|
1722
|
+
storage,
|
|
1723
|
+
relay,
|
|
1724
|
+
url,
|
|
1725
|
+
Array.from(pubKeys)
|
|
1726
|
+
).pipe(
|
|
1727
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1728
|
+
Effect8.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
|
|
1729
|
+
);
|
|
1730
|
+
yield* Effect8.logDebug("Relay reconciliation result", {
|
|
1731
|
+
relay: url,
|
|
1732
|
+
need: needIds.length,
|
|
1733
|
+
have: haveIds.length
|
|
1734
|
+
});
|
|
1735
|
+
const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
|
|
1736
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1737
|
+
Effect8.orElseSucceed(() => [])
|
|
1738
|
+
) : [];
|
|
1739
|
+
return {
|
|
1740
|
+
events,
|
|
1741
|
+
haveIds: haveIds.map((id) => ({ id, url }))
|
|
1742
|
+
};
|
|
1743
|
+
}).pipe(Effect8.withLogSpan("tablinum.reconcileRelay"));
|
|
1744
|
+
const syncAllRelays = (pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1745
|
+
const results = yield* Effect8.forEach(
|
|
1746
|
+
relayUrls,
|
|
1747
|
+
(url) => reconcileRelay(url, pubKeys),
|
|
1748
|
+
{ concurrency: "unbounded" }
|
|
1749
|
+
);
|
|
1750
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1751
|
+
const allGiftWraps = [];
|
|
1752
|
+
for (const { events } of results) {
|
|
1753
|
+
for (const ev of events) {
|
|
1754
|
+
if (!seen.has(ev.id)) {
|
|
1755
|
+
seen.add(ev.id);
|
|
1756
|
+
allGiftWraps.push(ev);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
const unwrapped = [];
|
|
1761
|
+
for (const gw of allGiftWraps) {
|
|
1762
|
+
const result = yield* unwrapGiftWrap(gw).pipe(Effect8.orElseSucceed(() => null));
|
|
1763
|
+
if (result) unwrapped.push(result);
|
|
1764
|
+
}
|
|
1765
|
+
unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
|
|
1766
|
+
for (const event of unwrapped) {
|
|
1767
|
+
const collection2 = yield* applyUnwrappedEvent(event).pipe(
|
|
1768
|
+
Effect8.orElseSucceed(() => null)
|
|
1769
|
+
);
|
|
1770
|
+
if (collection2) changedCollections.add(collection2);
|
|
1771
|
+
}
|
|
1772
|
+
const allHaveIds = results.flatMap((r) => r.haveIds);
|
|
1773
|
+
yield* Effect8.forEach(
|
|
1774
|
+
allHaveIds,
|
|
1775
|
+
({ id, url }) => Effect8.gen(function* () {
|
|
1776
|
+
const gw = yield* storage.getGiftWrap(id);
|
|
1777
|
+
if (!gw?.event) return;
|
|
1778
|
+
yield* relay.publish(gw.event, [url]).pipe(
|
|
1779
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1780
|
+
Effect8.ignore
|
|
1781
|
+
);
|
|
1782
|
+
}),
|
|
1783
|
+
{ concurrency: "unbounded", discard: true }
|
|
1784
|
+
);
|
|
1785
|
+
}).pipe(Effect8.withLogSpan("tablinum.syncAllRelays"));
|
|
1786
|
+
const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1787
|
+
const uw = yield* unwrapGiftWrap(remoteGw);
|
|
1788
|
+
if (!uw) return null;
|
|
1789
|
+
return yield* applyUnwrappedEvent(uw);
|
|
1790
|
+
});
|
|
1627
1791
|
const processRealtimeGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1628
1792
|
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
|
|
1629
1793
|
if (collection2) {
|
|
@@ -1631,32 +1795,34 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1631
1795
|
}
|
|
1632
1796
|
});
|
|
1633
1797
|
const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1634
|
-
const unwrapResult = yield* Effect8.result(
|
|
1635
|
-
try
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1798
|
+
const unwrapResult = yield* Effect8.result(
|
|
1799
|
+
Effect8.try({
|
|
1800
|
+
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1801
|
+
catch: (e) => new CryptoError({
|
|
1802
|
+
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
1803
|
+
cause: e
|
|
1804
|
+
})
|
|
1639
1805
|
})
|
|
1640
|
-
|
|
1641
|
-
if (unwrapResult._tag === "Failure")
|
|
1642
|
-
return false;
|
|
1806
|
+
);
|
|
1807
|
+
if (unwrapResult._tag === "Failure") return false;
|
|
1643
1808
|
const rumor = unwrapResult.success;
|
|
1644
1809
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1645
|
-
if (!dTag)
|
|
1646
|
-
return false;
|
|
1810
|
+
if (!dTag) return false;
|
|
1647
1811
|
const removalNoticeOpt = parseRemovalNotice(rumor.content, dTag);
|
|
1648
1812
|
if (Option5.isSome(removalNoticeOpt)) {
|
|
1649
|
-
if (onRemoved)
|
|
1650
|
-
onRemoved(removalNoticeOpt.value);
|
|
1813
|
+
if (onRemoved) onRemoved(removalNoticeOpt.value);
|
|
1651
1814
|
return true;
|
|
1652
1815
|
}
|
|
1653
1816
|
const rotationDataOpt = parseRotationEvent(rumor.content, dTag);
|
|
1654
|
-
if (Option5.isNone(rotationDataOpt))
|
|
1655
|
-
return false;
|
|
1817
|
+
if (Option5.isNone(rotationDataOpt)) return false;
|
|
1656
1818
|
const rotationData = rotationDataOpt.value;
|
|
1657
|
-
if (epochStore.epochs.has(rotationData.epochId))
|
|
1658
|
-
|
|
1659
|
-
|
|
1819
|
+
if (epochStore.epochs.has(rotationData.epochId)) return false;
|
|
1820
|
+
const epoch = createEpochKey(
|
|
1821
|
+
rotationData.epochId,
|
|
1822
|
+
rotationData.epochKey,
|
|
1823
|
+
rumor.pubkey || "",
|
|
1824
|
+
rotationData.parentEpoch
|
|
1825
|
+
);
|
|
1660
1826
|
addEpoch(epochStore, epoch);
|
|
1661
1827
|
epochStore.currentEpochId = epoch.id;
|
|
1662
1828
|
let membersChanged = false;
|
|
@@ -1676,79 +1842,107 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1676
1842
|
membersChanged = true;
|
|
1677
1843
|
}
|
|
1678
1844
|
}
|
|
1679
|
-
if (membersChanged && onMembersChanged)
|
|
1680
|
-
onMembersChanged();
|
|
1845
|
+
if (membersChanged && onMembersChanged) onMembersChanged();
|
|
1681
1846
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1682
1847
|
return true;
|
|
1683
1848
|
});
|
|
1684
|
-
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
if (
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
return;
|
|
1712
|
-
yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1713
|
-
}), { discard: true });
|
|
1714
|
-
}
|
|
1715
|
-
}).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
|
|
1849
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1850
|
+
relayUrls,
|
|
1851
|
+
(url) => Effect8.gen(function* () {
|
|
1852
|
+
yield* relay.subscribe(filter, url, (event) => {
|
|
1853
|
+
forkHandled(onEvent(event));
|
|
1854
|
+
}).pipe(
|
|
1855
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1856
|
+
Effect8.ignore
|
|
1857
|
+
);
|
|
1858
|
+
}),
|
|
1859
|
+
{ concurrency: "unbounded", discard: true }
|
|
1860
|
+
);
|
|
1861
|
+
let healingActive = false;
|
|
1862
|
+
const healingEffect = Effect8.gen(function* () {
|
|
1863
|
+
if (!healingActive) return;
|
|
1864
|
+
const status = yield* syncStatus.get();
|
|
1865
|
+
if (status === "syncing") return;
|
|
1866
|
+
yield* syncStatus.set("syncing");
|
|
1867
|
+
yield* Effect8.gen(function* () {
|
|
1868
|
+
const pubKeys = getSubscriptionPubKeys();
|
|
1869
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1870
|
+
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1871
|
+
if (changedCollections.size > 0) {
|
|
1872
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1873
|
+
}
|
|
1874
|
+
}).pipe(Effect8.ensuring(syncStatus.set("idle")));
|
|
1875
|
+
}).pipe(Effect8.ignore);
|
|
1716
1876
|
const handle = {
|
|
1717
1877
|
sync: () => Effect8.gen(function* () {
|
|
1718
1878
|
yield* Effect8.logInfo("Sync started");
|
|
1719
1879
|
yield* syncStatus.set("syncing");
|
|
1720
1880
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1721
|
-
const changedCollections = new Set;
|
|
1881
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1722
1882
|
yield* Effect8.gen(function* () {
|
|
1723
1883
|
const pubKeys = getSubscriptionPubKeys();
|
|
1724
|
-
yield*
|
|
1725
|
-
discard: true
|
|
1726
|
-
});
|
|
1884
|
+
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1727
1885
|
yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
|
|
1728
|
-
}).pipe(
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1886
|
+
}).pipe(
|
|
1887
|
+
Effect8.ensuring(
|
|
1888
|
+
Effect8.gen(function* () {
|
|
1889
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1890
|
+
yield* syncStatus.set("idle");
|
|
1891
|
+
})
|
|
1892
|
+
)
|
|
1893
|
+
);
|
|
1732
1894
|
yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
1733
1895
|
}).pipe(Effect8.withLogSpan("tablinum.sync")),
|
|
1734
1896
|
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1735
|
-
if (!giftWrap.event)
|
|
1736
|
-
|
|
1737
|
-
|
|
1897
|
+
if (!giftWrap.event) return;
|
|
1898
|
+
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1899
|
+
Effect8.tapError(
|
|
1900
|
+
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1901
|
+
Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
1902
|
+
Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
|
|
1903
|
+
)
|
|
1904
|
+
),
|
|
1905
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1906
|
+
Effect8.ignore
|
|
1907
|
+
);
|
|
1738
1908
|
}),
|
|
1739
1909
|
startSubscription: () => Effect8.gen(function* () {
|
|
1740
1910
|
const pubKeys = getSubscriptionPubKeys();
|
|
1741
1911
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1742
1912
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1743
|
-
yield* subscribeAcrossRelays(
|
|
1913
|
+
yield* subscribeAcrossRelays(
|
|
1914
|
+
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1915
|
+
(event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
|
|
1916
|
+
);
|
|
1744
1917
|
}
|
|
1745
1918
|
}),
|
|
1746
|
-
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap)
|
|
1919
|
+
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap),
|
|
1920
|
+
startHealing: () => {
|
|
1921
|
+
if (healingActive) return;
|
|
1922
|
+
healingActive = true;
|
|
1923
|
+
forkHandled(
|
|
1924
|
+
Effect8.sleep(Duration.minutes(5)).pipe(
|
|
1925
|
+
Effect8.andThen(healingEffect),
|
|
1926
|
+
Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
1927
|
+
Effect8.ensuring(Effect8.sync(() => {
|
|
1928
|
+
healingActive = false;
|
|
1929
|
+
}))
|
|
1930
|
+
)
|
|
1931
|
+
);
|
|
1932
|
+
},
|
|
1933
|
+
stopHealing: () => {
|
|
1934
|
+
healingActive = false;
|
|
1935
|
+
}
|
|
1747
1936
|
};
|
|
1748
|
-
forkHandled(
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1937
|
+
forkHandled(
|
|
1938
|
+
publishQueue.size().pipe(
|
|
1939
|
+
Effect8.flatMap(
|
|
1940
|
+
(size) => Effect8.sync(() => {
|
|
1941
|
+
if (size > 0) scheduleAutoFlush();
|
|
1942
|
+
})
|
|
1943
|
+
)
|
|
1944
|
+
)
|
|
1945
|
+
);
|
|
1752
1946
|
return handle;
|
|
1753
1947
|
}
|
|
1754
1948
|
|
|
@@ -1804,9 +1998,14 @@ var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(Aut
|
|
|
1804
1998
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1805
1999
|
return Effect9.gen(function* () {
|
|
1806
2000
|
for (const url of relayUrls) {
|
|
1807
|
-
const result = yield* Effect9.result(
|
|
2001
|
+
const result = yield* Effect9.result(
|
|
2002
|
+
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
2003
|
+
);
|
|
1808
2004
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
1809
|
-
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
2005
|
+
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
2006
|
+
Effect9.map(Option6.some),
|
|
2007
|
+
Effect9.orElseSucceed(() => Option6.none())
|
|
2008
|
+
);
|
|
1810
2009
|
}
|
|
1811
2010
|
}
|
|
1812
2011
|
return Option6.none();
|
|
@@ -1815,45 +2014,44 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
|
1815
2014
|
|
|
1816
2015
|
// src/services/Identity.ts
|
|
1817
2016
|
import { ServiceMap as ServiceMap3 } from "effect";
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
}
|
|
2017
|
+
var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
|
|
2018
|
+
};
|
|
1821
2019
|
|
|
1822
2020
|
// src/services/EpochStore.ts
|
|
1823
2021
|
import { ServiceMap as ServiceMap4 } from "effect";
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
2022
|
+
var EpochStore = class extends ServiceMap4.Service()(
|
|
2023
|
+
"tablinum/EpochStore"
|
|
2024
|
+
) {
|
|
2025
|
+
};
|
|
1827
2026
|
|
|
1828
2027
|
// src/services/Storage.ts
|
|
1829
2028
|
import { ServiceMap as ServiceMap5 } from "effect";
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
}
|
|
2029
|
+
var Storage = class extends ServiceMap5.Service()("tablinum/Storage") {
|
|
2030
|
+
};
|
|
1833
2031
|
|
|
1834
2032
|
// src/services/Relay.ts
|
|
1835
2033
|
import { ServiceMap as ServiceMap6 } from "effect";
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
}
|
|
2034
|
+
var Relay = class extends ServiceMap6.Service()("tablinum/Relay") {
|
|
2035
|
+
};
|
|
1839
2036
|
|
|
1840
2037
|
// src/services/GiftWrap.ts
|
|
1841
2038
|
import { ServiceMap as ServiceMap7 } from "effect";
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
}
|
|
2039
|
+
var GiftWrap3 = class extends ServiceMap7.Service()("tablinum/GiftWrap") {
|
|
2040
|
+
};
|
|
1845
2041
|
|
|
1846
2042
|
// src/services/PublishQueue.ts
|
|
1847
2043
|
import { ServiceMap as ServiceMap8 } from "effect";
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2044
|
+
var PublishQueue = class extends ServiceMap8.Service()(
|
|
2045
|
+
"tablinum/PublishQueue"
|
|
2046
|
+
) {
|
|
2047
|
+
};
|
|
1851
2048
|
|
|
1852
2049
|
// src/services/SyncStatus.ts
|
|
1853
2050
|
import { ServiceMap as ServiceMap9 } from "effect";
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2051
|
+
var SyncStatus = class extends ServiceMap9.Service()(
|
|
2052
|
+
"tablinum/SyncStatus"
|
|
2053
|
+
) {
|
|
2054
|
+
};
|
|
1857
2055
|
|
|
1858
2056
|
// src/layers/IdentityLive.ts
|
|
1859
2057
|
import { Effect as Effect11, Layer as Layer2 } from "effect";
|
|
@@ -1894,47 +2092,59 @@ function createIdentity(suppliedKey) {
|
|
|
1894
2092
|
}
|
|
1895
2093
|
|
|
1896
2094
|
// src/layers/IdentityLive.ts
|
|
1897
|
-
var IdentityLive = Layer2.effect(
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
})
|
|
2095
|
+
var IdentityLive = Layer2.effect(
|
|
2096
|
+
Identity,
|
|
2097
|
+
Effect11.gen(function* () {
|
|
2098
|
+
const config = yield* Config;
|
|
2099
|
+
const storage = yield* Storage;
|
|
2100
|
+
const idbKey = yield* storage.getMeta("identity_key");
|
|
2101
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
|
|
2102
|
+
const identity = yield* createIdentity(resolvedKey);
|
|
2103
|
+
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2104
|
+
yield* Effect11.logInfo("Identity loaded", {
|
|
2105
|
+
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2106
|
+
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2107
|
+
});
|
|
2108
|
+
return identity;
|
|
2109
|
+
})
|
|
2110
|
+
);
|
|
1910
2111
|
|
|
1911
2112
|
// src/layers/EpochStoreLive.ts
|
|
1912
2113
|
import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
|
|
1913
2114
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1914
2115
|
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
1915
|
-
var EpochStoreLive = Layer3.effect(
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
const
|
|
1922
|
-
if (
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
}
|
|
2116
|
+
var EpochStoreLive = Layer3.effect(
|
|
2117
|
+
EpochStore,
|
|
2118
|
+
Effect12.gen(function* () {
|
|
2119
|
+
const config = yield* Config;
|
|
2120
|
+
const identity = yield* Identity;
|
|
2121
|
+
const storage = yield* Storage;
|
|
2122
|
+
const idbRaw = yield* storage.getMeta("epochs");
|
|
2123
|
+
if (typeof idbRaw === "string") {
|
|
2124
|
+
const idbStore = deserializeEpochStore(idbRaw);
|
|
2125
|
+
if (Option7.isSome(idbStore)) {
|
|
2126
|
+
yield* Effect12.logInfo("Epoch store loaded", {
|
|
2127
|
+
source: "storage",
|
|
2128
|
+
epochs: idbStore.value.epochs.size
|
|
2129
|
+
});
|
|
2130
|
+
return idbStore.value;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
2134
|
+
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2135
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2136
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2137
|
+
return store2;
|
|
2138
|
+
}
|
|
2139
|
+
const store = createEpochStoreFromInputs(
|
|
2140
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
|
|
2141
|
+
{ createdBy: identity.publicKey }
|
|
2142
|
+
);
|
|
2143
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2144
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2145
|
+
return store;
|
|
2146
|
+
})
|
|
2147
|
+
);
|
|
1938
2148
|
|
|
1939
2149
|
// src/layers/StorageLive.ts
|
|
1940
2150
|
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
@@ -1942,6 +2152,68 @@ import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
|
1942
2152
|
// src/storage/idb.ts
|
|
1943
2153
|
import { Effect as Effect13 } from "effect";
|
|
1944
2154
|
import { openDB } from "idb";
|
|
2155
|
+
|
|
2156
|
+
// src/sync/compact-event.ts
|
|
2157
|
+
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2158
|
+
var VERSION = 1;
|
|
2159
|
+
var HEADER_SIZE = 133;
|
|
2160
|
+
function base64ToBytes(base64) {
|
|
2161
|
+
const binary = atob(base64);
|
|
2162
|
+
const bytes = new Uint8Array(binary.length);
|
|
2163
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2164
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2165
|
+
}
|
|
2166
|
+
return bytes;
|
|
2167
|
+
}
|
|
2168
|
+
function bytesToBase64(bytes) {
|
|
2169
|
+
let binary = "";
|
|
2170
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2171
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2172
|
+
}
|
|
2173
|
+
return btoa(binary);
|
|
2174
|
+
}
|
|
2175
|
+
function packEvent(event) {
|
|
2176
|
+
const pubkey = hexToBytes4(event.pubkey);
|
|
2177
|
+
const sig = hexToBytes4(event.sig);
|
|
2178
|
+
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2179
|
+
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2180
|
+
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2181
|
+
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2182
|
+
}
|
|
2183
|
+
const recipient = hexToBytes4(recipientTag[1]);
|
|
2184
|
+
const createdAtBuf = new Uint8Array(4);
|
|
2185
|
+
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2186
|
+
const content = base64ToBytes(event.content);
|
|
2187
|
+
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2188
|
+
result[0] = VERSION;
|
|
2189
|
+
result.set(pubkey, 1);
|
|
2190
|
+
result.set(sig, 33);
|
|
2191
|
+
result.set(recipient, 97);
|
|
2192
|
+
result.set(createdAtBuf, 129);
|
|
2193
|
+
result.set(content, HEADER_SIZE);
|
|
2194
|
+
return result;
|
|
2195
|
+
}
|
|
2196
|
+
function unpackEvent(id, compact) {
|
|
2197
|
+
const version = compact[0];
|
|
2198
|
+
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2199
|
+
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2200
|
+
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2201
|
+
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2202
|
+
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2203
|
+
const createdAt = dv.getUint32(0, false);
|
|
2204
|
+
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2205
|
+
return {
|
|
2206
|
+
id,
|
|
2207
|
+
pubkey,
|
|
2208
|
+
sig,
|
|
2209
|
+
created_at: createdAt,
|
|
2210
|
+
kind: 1059,
|
|
2211
|
+
tags: [["p", recipient]],
|
|
2212
|
+
content
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/storage/idb.ts
|
|
1945
2217
|
var DB_NAME = "tablinum";
|
|
1946
2218
|
function storeName(collection2) {
|
|
1947
2219
|
return `col_${collection2}`;
|
|
@@ -1972,7 +2244,7 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1972
2244
|
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
1973
2245
|
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
1974
2246
|
}
|
|
1975
|
-
const expectedStores = new Set;
|
|
2247
|
+
const expectedStores = /* @__PURE__ */ new Set();
|
|
1976
2248
|
for (const [, def] of Object.entries(schema)) {
|
|
1977
2249
|
const sn = storeName(def.name);
|
|
1978
2250
|
expectedStores.add(sn);
|
|
@@ -1986,12 +2258,10 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1986
2258
|
const existingIndices = new Set(Array.from(store.indexNames));
|
|
1987
2259
|
const wantedIndices = new Set(def.indices ?? []);
|
|
1988
2260
|
for (const idx of existingIndices) {
|
|
1989
|
-
if (!wantedIndices.has(idx))
|
|
1990
|
-
store.deleteIndex(idx);
|
|
2261
|
+
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
1991
2262
|
}
|
|
1992
2263
|
for (const idx of wantedIndices) {
|
|
1993
|
-
if (!existingIndices.has(idx))
|
|
1994
|
-
store.createIndex(idx, idx);
|
|
2264
|
+
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
1995
2265
|
}
|
|
1996
2266
|
}
|
|
1997
2267
|
}
|
|
@@ -2006,6 +2276,13 @@ function openIDBStorage(dbName, schema) {
|
|
|
2006
2276
|
return Effect13.gen(function* () {
|
|
2007
2277
|
const name = dbName ?? DB_NAME;
|
|
2008
2278
|
const schemaSig = computeSchemaSig(schema);
|
|
2279
|
+
if (typeof indexedDB === "undefined") {
|
|
2280
|
+
return yield* Effect13.fail(
|
|
2281
|
+
new StorageError({
|
|
2282
|
+
message: "IndexedDB is not available in this environment"
|
|
2283
|
+
})
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2009
2286
|
const probeDb = yield* Effect13.tryPromise({
|
|
2010
2287
|
try: () => openDB(name),
|
|
2011
2288
|
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
@@ -2016,7 +2293,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2016
2293
|
const storedSig = yield* Effect13.tryPromise({
|
|
2017
2294
|
try: () => probeDb.get("_meta", "schema_sig"),
|
|
2018
2295
|
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
2019
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(
|
|
2296
|
+
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
2020
2297
|
needsUpgrade = storedSig !== schemaSig;
|
|
2021
2298
|
}
|
|
2022
2299
|
probeDb.close();
|
|
@@ -2033,9 +2310,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2033
2310
|
});
|
|
2034
2311
|
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2035
2312
|
const handle = {
|
|
2036
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() =>
|
|
2037
|
-
return;
|
|
2038
|
-
})),
|
|
2313
|
+
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
2039
2314
|
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2040
2315
|
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2041
2316
|
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
@@ -2055,29 +2330,52 @@ function openIDBStorage(dbName, schema) {
|
|
|
2055
2330
|
}
|
|
2056
2331
|
return results;
|
|
2057
2332
|
}),
|
|
2058
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() =>
|
|
2059
|
-
return;
|
|
2060
|
-
})),
|
|
2333
|
+
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2061
2334
|
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2062
2335
|
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2063
|
-
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
const existing = await db.get("giftwraps", id);
|
|
2074
|
-
if (existing) {
|
|
2075
|
-
await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
|
|
2336
|
+
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2337
|
+
"getEventsByRecord",
|
|
2338
|
+
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2339
|
+
),
|
|
2340
|
+
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2341
|
+
if (gw.event) {
|
|
2342
|
+
const compact = packEvent(gw.event);
|
|
2343
|
+
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2344
|
+
} else {
|
|
2345
|
+
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2076
2346
|
}
|
|
2077
2347
|
}),
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2348
|
+
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2349
|
+
const raw = await db.get("giftwraps", id);
|
|
2350
|
+
if (!raw) return void 0;
|
|
2351
|
+
if (raw.compact) {
|
|
2352
|
+
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2353
|
+
}
|
|
2354
|
+
if (raw.event) {
|
|
2355
|
+
const compact = packEvent(raw.event);
|
|
2356
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2357
|
+
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2358
|
+
}
|
|
2359
|
+
return { id: raw.id, createdAt: raw.createdAt };
|
|
2360
|
+
}),
|
|
2361
|
+
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2362
|
+
const raws = await db.getAll("giftwraps");
|
|
2363
|
+
const results = [];
|
|
2364
|
+
for (const raw of raws) {
|
|
2365
|
+
if (raw.compact) {
|
|
2366
|
+
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2367
|
+
} else if (raw.event) {
|
|
2368
|
+
const compact = packEvent(raw.event);
|
|
2369
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2370
|
+
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2371
|
+
} else {
|
|
2372
|
+
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return results;
|
|
2376
|
+
}),
|
|
2377
|
+
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2378
|
+
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2081
2379
|
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2082
2380
|
const existing = await db.get("events", id);
|
|
2083
2381
|
if (existing) {
|
|
@@ -2085,9 +2383,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2085
2383
|
}
|
|
2086
2384
|
}),
|
|
2087
2385
|
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2088
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() =>
|
|
2089
|
-
return;
|
|
2090
|
-
})),
|
|
2386
|
+
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2091
2387
|
close: () => Effect13.sync(() => db.close())
|
|
2092
2388
|
};
|
|
2093
2389
|
return handle;
|
|
@@ -2095,15 +2391,18 @@ function openIDBStorage(dbName, schema) {
|
|
|
2095
2391
|
}
|
|
2096
2392
|
|
|
2097
2393
|
// src/layers/StorageLive.ts
|
|
2098
|
-
var StorageLive = Layer4.effect(
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
})
|
|
2394
|
+
var StorageLive = Layer4.effect(
|
|
2395
|
+
Storage,
|
|
2396
|
+
Effect14.gen(function* () {
|
|
2397
|
+
const config = yield* Config;
|
|
2398
|
+
const handle = yield* openIDBStorage(config.dbName, {
|
|
2399
|
+
...config.schema,
|
|
2400
|
+
_members: membersCollectionDef
|
|
2401
|
+
});
|
|
2402
|
+
yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
|
|
2403
|
+
return handle;
|
|
2404
|
+
})
|
|
2405
|
+
);
|
|
2107
2406
|
|
|
2108
2407
|
// src/layers/RelayLive.ts
|
|
2109
2408
|
import { Layer as Layer5 } from "effect";
|
|
@@ -2117,32 +2416,41 @@ var NegMessageFrameSchema = Schema7.Tuple([
|
|
|
2117
2416
|
Schema7.String
|
|
2118
2417
|
]);
|
|
2119
2418
|
var NegErrorFrameSchema = Schema7.Tuple([Schema7.Literal("NEG-ERR"), Schema7.String, Schema7.String]);
|
|
2120
|
-
var decodeNegFrame = Schema7.decodeUnknownEffect(
|
|
2419
|
+
var decodeNegFrame = Schema7.decodeUnknownEffect(
|
|
2420
|
+
Schema7.fromJsonString(Schema7.Union([NegMessageFrameSchema, NegErrorFrameSchema]))
|
|
2421
|
+
);
|
|
2121
2422
|
function parseNegMessageFrame(data) {
|
|
2122
|
-
return Effect15.runSync(
|
|
2423
|
+
return Effect15.runSync(
|
|
2424
|
+
decodeNegFrame(data).pipe(
|
|
2425
|
+
Effect15.map(Option8.some),
|
|
2426
|
+
Effect15.orElseSucceed(() => Option8.none())
|
|
2427
|
+
)
|
|
2428
|
+
);
|
|
2123
2429
|
}
|
|
2124
2430
|
function createRelayHandle() {
|
|
2125
2431
|
return Effect15.gen(function* () {
|
|
2126
2432
|
const relayScope = yield* Effect15.scope;
|
|
2127
2433
|
const connections = yield* ScopedCache.make({
|
|
2128
2434
|
capacity: 64,
|
|
2129
|
-
lookup: (url) => Effect15.acquireRelease(
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2435
|
+
lookup: (url) => Effect15.acquireRelease(
|
|
2436
|
+
Effect15.tryPromise({
|
|
2437
|
+
try: () => Relay2.connect(url),
|
|
2438
|
+
catch: (e) => new RelayError({
|
|
2439
|
+
message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2440
|
+
url,
|
|
2441
|
+
cause: e
|
|
2442
|
+
})
|
|
2443
|
+
}),
|
|
2444
|
+
(relay) => Effect15.sync(() => {
|
|
2445
|
+
relay.close();
|
|
2135
2446
|
})
|
|
2136
|
-
|
|
2137
|
-
relay.close();
|
|
2138
|
-
}))
|
|
2447
|
+
)
|
|
2139
2448
|
});
|
|
2140
|
-
const connectedUrls = new Set;
|
|
2141
|
-
const statusListeners = new Set;
|
|
2449
|
+
const connectedUrls = /* @__PURE__ */ new Set();
|
|
2450
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
2142
2451
|
const notifyStatus = () => {
|
|
2143
2452
|
const status = { connectedUrls: [...connectedUrls] };
|
|
2144
|
-
for (const listener of statusListeners)
|
|
2145
|
-
listener(status);
|
|
2453
|
+
for (const listener of statusListeners) listener(status);
|
|
2146
2454
|
};
|
|
2147
2455
|
const markConnected = (url) => {
|
|
2148
2456
|
if (!connectedUrls.has(url)) {
|
|
@@ -2156,66 +2464,98 @@ function createRelayHandle() {
|
|
|
2156
2464
|
notifyStatus();
|
|
2157
2465
|
}
|
|
2158
2466
|
};
|
|
2159
|
-
const getRelay = (url) => ScopedCache.get(connections, url).pipe(
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
sub
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2467
|
+
const getRelay = (url) => ScopedCache.get(connections, url).pipe(
|
|
2468
|
+
Effect15.flatMap(
|
|
2469
|
+
(relay) => relay.connected === false ? ScopedCache.invalidate(connections, url).pipe(
|
|
2470
|
+
Effect15.andThen(ScopedCache.get(connections, url))
|
|
2471
|
+
) : Effect15.succeed(relay)
|
|
2472
|
+
)
|
|
2473
|
+
);
|
|
2474
|
+
const withRelay = (url, run) => getRelay(url).pipe(
|
|
2475
|
+
Effect15.tap(() => Effect15.sync(() => markConnected(url))),
|
|
2476
|
+
Effect15.flatMap((relay) => run(relay)),
|
|
2477
|
+
Effect15.tapError(
|
|
2478
|
+
() => ScopedCache.invalidate(connections, url).pipe(
|
|
2479
|
+
Effect15.tap(() => Effect15.sync(() => markDisconnected(url)))
|
|
2480
|
+
)
|
|
2481
|
+
)
|
|
2482
|
+
);
|
|
2483
|
+
const collectEvents = (url, filters) => withRelay(
|
|
2484
|
+
url,
|
|
2485
|
+
(relay) => Effect15.callback((resume) => {
|
|
2486
|
+
const events = [];
|
|
2487
|
+
let settled = false;
|
|
2488
|
+
let timer;
|
|
2489
|
+
let sub;
|
|
2490
|
+
const cleanup = () => {
|
|
2491
|
+
settled = true;
|
|
2492
|
+
if (timer !== void 0) {
|
|
2493
|
+
clearTimeout(timer);
|
|
2494
|
+
timer = void 0;
|
|
2495
|
+
}
|
|
2496
|
+
sub?.close();
|
|
2497
|
+
sub = void 0;
|
|
2498
|
+
};
|
|
2499
|
+
const fail = (e) => resume(
|
|
2500
|
+
Effect15.fail(
|
|
2501
|
+
new RelayError({
|
|
2502
|
+
message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2503
|
+
url,
|
|
2504
|
+
cause: e
|
|
2505
|
+
})
|
|
2506
|
+
)
|
|
2507
|
+
);
|
|
2508
|
+
try {
|
|
2509
|
+
sub = relay.subscribe([...filters], {
|
|
2510
|
+
onevent(evt) {
|
|
2511
|
+
if (!settled) {
|
|
2512
|
+
events.push(evt);
|
|
2513
|
+
}
|
|
2514
|
+
},
|
|
2515
|
+
oneose() {
|
|
2516
|
+
if (settled) return;
|
|
2517
|
+
cleanup();
|
|
2518
|
+
resume(Effect15.succeed(events));
|
|
2185
2519
|
}
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
if (settled)
|
|
2189
|
-
return;
|
|
2520
|
+
});
|
|
2521
|
+
timer = setTimeout(() => {
|
|
2522
|
+
if (settled) return;
|
|
2190
2523
|
cleanup();
|
|
2191
2524
|
resume(Effect15.succeed(events));
|
|
2192
|
-
}
|
|
2193
|
-
})
|
|
2194
|
-
timer = setTimeout(() => {
|
|
2195
|
-
if (settled)
|
|
2196
|
-
return;
|
|
2525
|
+
}, 1e4);
|
|
2526
|
+
} catch (e) {
|
|
2197
2527
|
cleanup();
|
|
2198
|
-
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
}
|
|
2204
|
-
return Effect15.sync(cleanup);
|
|
2205
|
-
}));
|
|
2528
|
+
fail(e);
|
|
2529
|
+
}
|
|
2530
|
+
return Effect15.sync(cleanup);
|
|
2531
|
+
})
|
|
2532
|
+
);
|
|
2206
2533
|
return {
|
|
2207
2534
|
publish: (event, urls) => Effect15.gen(function* () {
|
|
2208
|
-
const results = yield* Effect15.forEach(
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2535
|
+
const results = yield* Effect15.forEach(
|
|
2536
|
+
urls,
|
|
2537
|
+
(url) => Effect15.result(
|
|
2538
|
+
withRelay(
|
|
2539
|
+
url,
|
|
2540
|
+
(relay) => Effect15.tryPromise({
|
|
2541
|
+
try: () => relay.publish(event),
|
|
2542
|
+
catch: (e) => new RelayError({
|
|
2543
|
+
message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2544
|
+
url,
|
|
2545
|
+
cause: e
|
|
2546
|
+
})
|
|
2547
|
+
}).pipe(
|
|
2548
|
+
Effect15.timeoutOrElse({
|
|
2549
|
+
duration: "10 seconds",
|
|
2550
|
+
onTimeout: () => Effect15.fail(
|
|
2551
|
+
new RelayError({ message: `Publish to ${url} timed out`, url })
|
|
2552
|
+
)
|
|
2553
|
+
})
|
|
2554
|
+
)
|
|
2555
|
+
)
|
|
2556
|
+
),
|
|
2557
|
+
{ concurrency: "unbounded" }
|
|
2558
|
+
);
|
|
2219
2559
|
const failures = results.filter((r) => r._tag === "Failure");
|
|
2220
2560
|
if (failures.length === urls.length && urls.length > 0) {
|
|
2221
2561
|
return yield* new RelayError({
|
|
@@ -2224,90 +2564,104 @@ function createRelayHandle() {
|
|
|
2224
2564
|
}
|
|
2225
2565
|
}),
|
|
2226
2566
|
fetchEvents: (ids, url) => Effect15.gen(function* () {
|
|
2227
|
-
if (ids.length === 0)
|
|
2228
|
-
return [];
|
|
2567
|
+
if (ids.length === 0) return [];
|
|
2229
2568
|
return yield* collectEvents(url, [{ ids }]);
|
|
2230
2569
|
}),
|
|
2231
2570
|
fetchByFilter: (filter, url) => collectEvents(url, [filter]),
|
|
2232
|
-
subscribe: (filter, url, onEvent) => withRelay(
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
sub
|
|
2260
|
-
ws
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
return;
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
try {
|
|
2287
|
-
sub = relay.subscribe([filter], {
|
|
2288
|
-
onevent() {},
|
|
2289
|
-
oneose() {}
|
|
2290
|
-
});
|
|
2291
|
-
ws = relay._ws || relay.ws;
|
|
2292
|
-
if (!ws) {
|
|
2571
|
+
subscribe: (filter, url, onEvent) => withRelay(
|
|
2572
|
+
url,
|
|
2573
|
+
(relay) => Effect15.acquireRelease(
|
|
2574
|
+
Effect15.try({
|
|
2575
|
+
try: () => relay.subscribe([filter], {
|
|
2576
|
+
onevent(evt) {
|
|
2577
|
+
onEvent(evt);
|
|
2578
|
+
},
|
|
2579
|
+
oneose() {
|
|
2580
|
+
}
|
|
2581
|
+
}),
|
|
2582
|
+
catch: (e) => new RelayError({
|
|
2583
|
+
message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2584
|
+
url,
|
|
2585
|
+
cause: e
|
|
2586
|
+
})
|
|
2587
|
+
}),
|
|
2588
|
+
(sub) => Effect15.sync(() => {
|
|
2589
|
+
sub.close();
|
|
2590
|
+
})
|
|
2591
|
+
).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)
|
|
2592
|
+
),
|
|
2593
|
+
sendNegMsg: (url, subId, filter, msgHex) => withRelay(
|
|
2594
|
+
url,
|
|
2595
|
+
(relay) => Effect15.callback((resume) => {
|
|
2596
|
+
let settled = false;
|
|
2597
|
+
let timer;
|
|
2598
|
+
let sub;
|
|
2599
|
+
let ws;
|
|
2600
|
+
const cleanup = () => {
|
|
2601
|
+
settled = true;
|
|
2602
|
+
if (timer !== void 0) {
|
|
2603
|
+
clearTimeout(timer);
|
|
2604
|
+
timer = void 0;
|
|
2605
|
+
}
|
|
2606
|
+
sub?.close();
|
|
2607
|
+
sub = void 0;
|
|
2608
|
+
ws?.removeEventListener("message", handler);
|
|
2609
|
+
ws = void 0;
|
|
2610
|
+
};
|
|
2611
|
+
const fail = (e) => resume(
|
|
2612
|
+
Effect15.fail(
|
|
2613
|
+
new RelayError({
|
|
2614
|
+
message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2615
|
+
url,
|
|
2616
|
+
cause: e
|
|
2617
|
+
})
|
|
2618
|
+
)
|
|
2619
|
+
);
|
|
2620
|
+
const handler = (msg) => {
|
|
2621
|
+
if (settled || typeof msg.data !== "string") return;
|
|
2622
|
+
const frameOpt = parseNegMessageFrame(msg.data);
|
|
2623
|
+
if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId) return;
|
|
2624
|
+
const frame = frameOpt.value;
|
|
2293
2625
|
cleanup();
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2626
|
+
if (frame[0] === "NEG-MSG") {
|
|
2627
|
+
resume(
|
|
2628
|
+
Effect15.succeed({
|
|
2629
|
+
msgHex: frame[2],
|
|
2630
|
+
haveIds: [],
|
|
2631
|
+
needIds: []
|
|
2632
|
+
})
|
|
2633
|
+
);
|
|
2299
2634
|
return;
|
|
2635
|
+
}
|
|
2636
|
+
fail(new Error(`NEG-ERR: ${frame[2]}`));
|
|
2637
|
+
};
|
|
2638
|
+
try {
|
|
2639
|
+
sub = relay.subscribe([filter], {
|
|
2640
|
+
onevent() {
|
|
2641
|
+
},
|
|
2642
|
+
oneose() {
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
ws = relay._ws || relay.ws;
|
|
2646
|
+
if (!ws) {
|
|
2647
|
+
cleanup();
|
|
2648
|
+
fail(new Error("Cannot access relay WebSocket"));
|
|
2649
|
+
return Effect15.succeed(void 0);
|
|
2650
|
+
}
|
|
2651
|
+
timer = setTimeout(() => {
|
|
2652
|
+
if (settled) return;
|
|
2653
|
+
cleanup();
|
|
2654
|
+
fail(new Error("NIP-77 negotiation timeout"));
|
|
2655
|
+
}, 3e4);
|
|
2656
|
+
ws.addEventListener("message", handler);
|
|
2657
|
+
ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
|
|
2658
|
+
} catch (e) {
|
|
2300
2659
|
cleanup();
|
|
2301
|
-
fail(
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
cleanup();
|
|
2307
|
-
fail(e);
|
|
2308
|
-
}
|
|
2309
|
-
return Effect15.sync(cleanup);
|
|
2310
|
-
})),
|
|
2660
|
+
fail(e);
|
|
2661
|
+
}
|
|
2662
|
+
return Effect15.sync(cleanup);
|
|
2663
|
+
})
|
|
2664
|
+
),
|
|
2311
2665
|
closeAll: () => ScopedCache.invalidateAll(connections),
|
|
2312
2666
|
getStatus: () => ({ connectedUrls: [...connectedUrls] }),
|
|
2313
2667
|
subscribeStatus: (callback) => {
|
|
@@ -2359,11 +2713,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
|
2359
2713
|
}
|
|
2360
2714
|
|
|
2361
2715
|
// src/layers/GiftWrapLive.ts
|
|
2362
|
-
var GiftWrapLive = Layer6.effect(
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2716
|
+
var GiftWrapLive = Layer6.effect(
|
|
2717
|
+
GiftWrap3,
|
|
2718
|
+
Effect17.gen(function* () {
|
|
2719
|
+
const identity = yield* Identity;
|
|
2720
|
+
const epochStore = yield* EpochStore;
|
|
2721
|
+
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2722
|
+
})
|
|
2723
|
+
);
|
|
2367
2724
|
|
|
2368
2725
|
// src/layers/PublishQueueLive.ts
|
|
2369
2726
|
import { Effect as Effect19, Layer as Layer7 } from "effect";
|
|
@@ -2377,12 +2734,11 @@ function persist(storage, pending) {
|
|
|
2377
2734
|
function createPublishQueue(storage, relay) {
|
|
2378
2735
|
return Effect18.gen(function* () {
|
|
2379
2736
|
const stored = yield* storage.getMeta(META_KEY);
|
|
2380
|
-
const initial = Array.isArray(stored) ? new Set(stored) : new Set;
|
|
2737
|
+
const initial = Array.isArray(stored) ? new Set(stored) : /* @__PURE__ */ new Set();
|
|
2381
2738
|
const pendingRef = yield* Ref4.make(initial);
|
|
2382
|
-
const listeners = new Set;
|
|
2739
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2383
2740
|
const notify = (pending) => {
|
|
2384
|
-
for (const listener of listeners)
|
|
2385
|
-
listener(pending.size);
|
|
2741
|
+
for (const listener of listeners) listener(pending.size);
|
|
2386
2742
|
};
|
|
2387
2743
|
return {
|
|
2388
2744
|
enqueue: (eventId) => Effect18.gen(function* () {
|
|
@@ -2396,13 +2752,11 @@ function createPublishQueue(storage, relay) {
|
|
|
2396
2752
|
}),
|
|
2397
2753
|
flush: (relayUrls) => Effect18.gen(function* () {
|
|
2398
2754
|
const pending = yield* Ref4.get(pendingRef);
|
|
2399
|
-
if (pending.size === 0)
|
|
2400
|
-
|
|
2401
|
-
const succeeded = new Set;
|
|
2755
|
+
if (pending.size === 0) return;
|
|
2756
|
+
const succeeded = /* @__PURE__ */ new Set();
|
|
2402
2757
|
let consecutiveFailures = 0;
|
|
2403
2758
|
for (const eventId of pending) {
|
|
2404
|
-
if (consecutiveFailures >= 3)
|
|
2405
|
-
break;
|
|
2759
|
+
if (consecutiveFailures >= 3) break;
|
|
2406
2760
|
const gw = yield* storage.getGiftWrap(eventId);
|
|
2407
2761
|
if (!gw || !gw.event) {
|
|
2408
2762
|
succeeded.add(eventId);
|
|
@@ -2412,7 +2766,6 @@ function createPublishQueue(storage, relay) {
|
|
|
2412
2766
|
const result = yield* Effect18.result(relay.publish(gw.event, relayUrls));
|
|
2413
2767
|
if (result._tag === "Success") {
|
|
2414
2768
|
succeeded.add(eventId);
|
|
2415
|
-
yield* storage.stripGiftWrapBlob(eventId);
|
|
2416
2769
|
consecutiveFailures = 0;
|
|
2417
2770
|
} else {
|
|
2418
2771
|
consecutiveFailures++;
|
|
@@ -2440,11 +2793,14 @@ function createPublishQueue(storage, relay) {
|
|
|
2440
2793
|
}
|
|
2441
2794
|
|
|
2442
2795
|
// src/layers/PublishQueueLive.ts
|
|
2443
|
-
var PublishQueueLive = Layer7.effect(
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2796
|
+
var PublishQueueLive = Layer7.effect(
|
|
2797
|
+
PublishQueue,
|
|
2798
|
+
Effect19.gen(function* () {
|
|
2799
|
+
const storage = yield* Storage;
|
|
2800
|
+
const relay = yield* Relay;
|
|
2801
|
+
return yield* createPublishQueue(storage, relay);
|
|
2802
|
+
})
|
|
2803
|
+
);
|
|
2448
2804
|
|
|
2449
2805
|
// src/layers/SyncStatusLive.ts
|
|
2450
2806
|
import { Layer as Layer8 } from "effect";
|
|
@@ -2454,13 +2810,12 @@ import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
|
2454
2810
|
function createSyncStatusHandle() {
|
|
2455
2811
|
return Effect20.gen(function* () {
|
|
2456
2812
|
const ref = yield* SubscriptionRef.make("idle");
|
|
2457
|
-
const listeners = new Set;
|
|
2813
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2458
2814
|
return {
|
|
2459
2815
|
get: () => SubscriptionRef.get(ref),
|
|
2460
2816
|
set: (status) => Effect20.gen(function* () {
|
|
2461
2817
|
yield* SubscriptionRef.set(ref, status);
|
|
2462
|
-
for (const listener of listeners)
|
|
2463
|
-
listener(status);
|
|
2818
|
+
for (const listener of listeners) listener(status);
|
|
2464
2819
|
}),
|
|
2465
2820
|
subscribe: (callback) => {
|
|
2466
2821
|
listeners.add(callback);
|
|
@@ -2475,8 +2830,7 @@ var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
|
|
|
2475
2830
|
|
|
2476
2831
|
// src/layers/TablinumLive.ts
|
|
2477
2832
|
function reportSyncError(onSyncError, error) {
|
|
2478
|
-
if (!onSyncError)
|
|
2479
|
-
return;
|
|
2833
|
+
if (!onSyncError) return;
|
|
2480
2834
|
onSyncError(error instanceof Error ? error : new Error(String(error)));
|
|
2481
2835
|
}
|
|
2482
2836
|
function mapMemberRecord(record) {
|
|
@@ -2484,249 +2838,352 @@ function mapMemberRecord(record) {
|
|
|
2484
2838
|
id: record.id,
|
|
2485
2839
|
addedAt: record.addedAt,
|
|
2486
2840
|
addedInEpoch: record.addedInEpoch,
|
|
2487
|
-
...record.name !==
|
|
2488
|
-
...record.picture !==
|
|
2489
|
-
...record.about !==
|
|
2490
|
-
...record.nip05 !==
|
|
2491
|
-
...record.removedAt !==
|
|
2492
|
-
...record.removedInEpoch !==
|
|
2841
|
+
...record.name !== void 0 ? { name: record.name } : {},
|
|
2842
|
+
...record.picture !== void 0 ? { picture: record.picture } : {},
|
|
2843
|
+
...record.about !== void 0 ? { about: record.about } : {},
|
|
2844
|
+
...record.nip05 !== void 0 ? { nip05: record.nip05 } : {},
|
|
2845
|
+
...record.removedAt !== void 0 ? { removedAt: record.removedAt } : {},
|
|
2846
|
+
...record.removedInEpoch !== void 0 ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2493
2847
|
};
|
|
2494
2848
|
}
|
|
2495
2849
|
var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
|
|
2496
|
-
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
var
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
const
|
|
2522
|
-
const
|
|
2523
|
-
const
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
const
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2850
|
+
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2851
|
+
Layer9.provide(IdentityWithDeps),
|
|
2852
|
+
Layer9.provide(StorageLive)
|
|
2853
|
+
);
|
|
2854
|
+
var GiftWrapWithDeps = GiftWrapLive.pipe(
|
|
2855
|
+
Layer9.provide(IdentityWithDeps),
|
|
2856
|
+
Layer9.provide(EpochStoreWithDeps)
|
|
2857
|
+
);
|
|
2858
|
+
var PublishQueueWithDeps = PublishQueueLive.pipe(
|
|
2859
|
+
Layer9.provide(StorageLive),
|
|
2860
|
+
Layer9.provide(RelayLive)
|
|
2861
|
+
);
|
|
2862
|
+
var AllServicesLive = Layer9.mergeAll(
|
|
2863
|
+
IdentityWithDeps,
|
|
2864
|
+
EpochStoreWithDeps,
|
|
2865
|
+
StorageLive,
|
|
2866
|
+
RelayLive,
|
|
2867
|
+
GiftWrapWithDeps,
|
|
2868
|
+
PublishQueueWithDeps,
|
|
2869
|
+
SyncStatusLive
|
|
2870
|
+
);
|
|
2871
|
+
var TablinumLive = Layer9.effect(
|
|
2872
|
+
Tablinum,
|
|
2873
|
+
Effect21.gen(function* () {
|
|
2874
|
+
const config = yield* Config;
|
|
2875
|
+
const identity = yield* Identity;
|
|
2876
|
+
const epochStore = yield* EpochStore;
|
|
2877
|
+
const storage = yield* Storage;
|
|
2878
|
+
const relay = yield* Relay;
|
|
2879
|
+
const giftWrap = yield* GiftWrap3;
|
|
2880
|
+
const publishQueue = yield* PublishQueue;
|
|
2881
|
+
const syncStatus = yield* SyncStatus;
|
|
2882
|
+
const scope = yield* Effect21.scope;
|
|
2883
|
+
const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
|
|
2884
|
+
const pubsub = yield* PubSub2.unbounded();
|
|
2885
|
+
const replayingRef = yield* Ref5.make(false);
|
|
2886
|
+
const closedRef = yield* Ref5.make(false);
|
|
2887
|
+
const watchCtx = { pubsub, replayingRef };
|
|
2888
|
+
const schemaEntries = Object.entries(config.schema);
|
|
2889
|
+
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2890
|
+
const knownCollections = new Map(
|
|
2891
|
+
allSchemaEntries.map(([, def]) => [def.name, def.eventRetention])
|
|
2892
|
+
);
|
|
2893
|
+
let notifyAuthor;
|
|
2894
|
+
const syncHandle = createSyncHandle(
|
|
2895
|
+
storage,
|
|
2896
|
+
giftWrap,
|
|
2897
|
+
relay,
|
|
2898
|
+
publishQueue,
|
|
2899
|
+
syncStatus,
|
|
2900
|
+
watchCtx,
|
|
2901
|
+
config.relays,
|
|
2902
|
+
knownCollections,
|
|
2903
|
+
epochStore,
|
|
2904
|
+
identity.privateKey,
|
|
2905
|
+
identity.publicKey,
|
|
2906
|
+
scope,
|
|
2907
|
+
config.logLevel,
|
|
2908
|
+
config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : void 0,
|
|
2909
|
+
(pubkey) => notifyAuthor?.(pubkey),
|
|
2910
|
+
config.onRemoved,
|
|
2911
|
+
config.onMembersChanged
|
|
2912
|
+
);
|
|
2913
|
+
const onWrite = (event) => Effect21.gen(function* () {
|
|
2914
|
+
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2915
|
+
const dTag = `${event.collection}:${event.recordId}`;
|
|
2916
|
+
const wrapResult = yield* Effect21.result(
|
|
2917
|
+
giftWrap.wrap({
|
|
2918
|
+
kind: 1,
|
|
2919
|
+
content,
|
|
2920
|
+
tags: [["d", dTag]],
|
|
2921
|
+
created_at: Math.floor(event.createdAt / 1e3)
|
|
2922
|
+
})
|
|
2923
|
+
);
|
|
2924
|
+
if (wrapResult._tag === "Failure") {
|
|
2925
|
+
reportSyncError(config.onSyncError, wrapResult.failure);
|
|
2926
|
+
return;
|
|
2543
2927
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
yield* notifyChange(watchCtx, {
|
|
2562
|
-
collection: "_members",
|
|
2563
|
-
recordId: record.id,
|
|
2564
|
-
kind: existing ? "update" : "create"
|
|
2928
|
+
const gw = wrapResult.success;
|
|
2929
|
+
yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
2930
|
+
yield* Effect21.forkIn(
|
|
2931
|
+
Effect21.gen(function* () {
|
|
2932
|
+
const publishResult = yield* Effect21.result(
|
|
2933
|
+
syncHandle.publishLocal({
|
|
2934
|
+
id: gw.id,
|
|
2935
|
+
event: gw,
|
|
2936
|
+
createdAt: gw.created_at
|
|
2937
|
+
})
|
|
2938
|
+
);
|
|
2939
|
+
if (publishResult._tag === "Failure") {
|
|
2940
|
+
reportSyncError(config.onSyncError, publishResult.failure);
|
|
2941
|
+
}
|
|
2942
|
+
}),
|
|
2943
|
+
scope
|
|
2944
|
+
);
|
|
2565
2945
|
});
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
...profileOpt.value
|
|
2588
|
-
});
|
|
2589
|
-
yield* notifyChange(watchCtx, {
|
|
2590
|
-
collection: "_members",
|
|
2591
|
-
recordId: pubkey,
|
|
2592
|
-
kind: "update"
|
|
2593
|
-
});
|
|
2594
|
-
config.onMembersChanged?.();
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope)));
|
|
2598
|
-
};
|
|
2599
|
-
const handles = new Map;
|
|
2600
|
-
for (const [, def] of allSchemaEntries) {
|
|
2601
|
-
const validator = buildValidator(def.name, def);
|
|
2602
|
-
const partialValidator = buildPartialValidator(def.name, def);
|
|
2603
|
-
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite, config.logLevel);
|
|
2604
|
-
handles.set(def.name, handle);
|
|
2605
|
-
}
|
|
2606
|
-
yield* syncHandle.startSubscription();
|
|
2607
|
-
yield* Effect21.logInfo("Tablinum ready", {
|
|
2608
|
-
dbName: config.dbName,
|
|
2609
|
-
collections: schemaEntries.map(([name]) => name),
|
|
2610
|
-
relays: config.relays
|
|
2611
|
-
});
|
|
2612
|
-
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2613
|
-
if (!selfMember) {
|
|
2614
|
-
yield* putMemberRecord({
|
|
2615
|
-
id: identity.publicKey,
|
|
2616
|
-
addedAt: Date.now(),
|
|
2617
|
-
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2946
|
+
const knownAuthors = /* @__PURE__ */ new Set();
|
|
2947
|
+
const putMemberRecord = (record) => Effect21.gen(function* () {
|
|
2948
|
+
const existing = yield* storage.getRecord("_members", record.id);
|
|
2949
|
+
const event = {
|
|
2950
|
+
id: uuidv7(),
|
|
2951
|
+
collection: "_members",
|
|
2952
|
+
recordId: record.id,
|
|
2953
|
+
kind: existing ? "u" : "c",
|
|
2954
|
+
data: record,
|
|
2955
|
+
createdAt: Date.now(),
|
|
2956
|
+
author: identity.publicKey
|
|
2957
|
+
};
|
|
2958
|
+
yield* storage.putEvent(event);
|
|
2959
|
+
yield* applyEvent(storage, event);
|
|
2960
|
+
yield* onWrite(event);
|
|
2961
|
+
yield* notifyChange(watchCtx, {
|
|
2962
|
+
collection: "_members",
|
|
2963
|
+
recordId: record.id,
|
|
2964
|
+
kind: existing ? "update" : "create"
|
|
2965
|
+
});
|
|
2966
|
+
config.onMembersChanged?.();
|
|
2618
2967
|
});
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2968
|
+
notifyAuthor = (pubkey) => {
|
|
2969
|
+
if (knownAuthors.has(pubkey)) return;
|
|
2970
|
+
knownAuthors.add(pubkey);
|
|
2971
|
+
Effect21.runFork(
|
|
2972
|
+
Effect21.gen(function* () {
|
|
2973
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
2974
|
+
if (!existing) {
|
|
2975
|
+
yield* putMemberRecord({
|
|
2976
|
+
id: pubkey,
|
|
2977
|
+
addedAt: Date.now(),
|
|
2978
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(
|
|
2982
|
+
Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none()))
|
|
2983
|
+
);
|
|
2984
|
+
if (Option9.isSome(profileOpt)) {
|
|
2985
|
+
const current = yield* storage.getRecord("_members", pubkey);
|
|
2986
|
+
if (current) {
|
|
2987
|
+
yield* storage.putRecord("_members", {
|
|
2988
|
+
...current,
|
|
2989
|
+
...profileOpt.value
|
|
2990
|
+
});
|
|
2991
|
+
yield* notifyChange(watchCtx, {
|
|
2992
|
+
collection: "_members",
|
|
2993
|
+
recordId: pubkey,
|
|
2994
|
+
kind: "update"
|
|
2995
|
+
});
|
|
2996
|
+
config.onMembersChanged?.();
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope))
|
|
3000
|
+
);
|
|
3001
|
+
};
|
|
3002
|
+
const handles = /* @__PURE__ */ new Map();
|
|
3003
|
+
for (const [, def] of allSchemaEntries) {
|
|
3004
|
+
const validator = buildValidator(def.name, def);
|
|
3005
|
+
const partialValidator = buildPartialValidator(def.name, def);
|
|
3006
|
+
const handle = createCollectionHandle(
|
|
3007
|
+
def,
|
|
3008
|
+
storage,
|
|
3009
|
+
watchCtx,
|
|
3010
|
+
validator,
|
|
3011
|
+
partialValidator,
|
|
3012
|
+
uuidv7,
|
|
3013
|
+
identity.publicKey,
|
|
3014
|
+
onWrite,
|
|
3015
|
+
config.logLevel
|
|
3016
|
+
);
|
|
3017
|
+
handles.set(def.name, handle);
|
|
3018
|
+
}
|
|
3019
|
+
yield* syncHandle.startSubscription();
|
|
3020
|
+
yield* Effect21.logInfo("Tablinum ready", {
|
|
3021
|
+
dbName: config.dbName,
|
|
3022
|
+
collections: schemaEntries.map(([name]) => name),
|
|
3023
|
+
relays: config.relays
|
|
3024
|
+
});
|
|
3025
|
+
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
3026
|
+
if (!selfMember) {
|
|
2666
3027
|
yield* putMemberRecord({
|
|
2667
|
-
id:
|
|
3028
|
+
id: identity.publicKey,
|
|
2668
3029
|
addedAt: Date.now(),
|
|
2669
|
-
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2670
|
-
...existing ? { removedAt: undefined, removedInEpoch: undefined } : {}
|
|
2671
|
-
});
|
|
2672
|
-
})),
|
|
2673
|
-
removeMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
|
|
2674
|
-
const allMembers = yield* storage.getAllRecords("_members");
|
|
2675
|
-
const activeMembers = allMembers.filter((member) => !member.removedAt && member.id !== pubkey);
|
|
2676
|
-
const activePubkeys = activeMembers.map((member) => member.id);
|
|
2677
|
-
const result = createRotation(epochStore, identity.privateKey, identity.publicKey, activePubkeys, [pubkey]);
|
|
2678
|
-
addEpoch(epochStore, result.epoch);
|
|
2679
|
-
epochStore.currentEpochId = result.epoch.id;
|
|
2680
|
-
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
2681
|
-
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
2682
|
-
yield* putMemberRecord({
|
|
2683
|
-
...memberRecord ?? {
|
|
2684
|
-
id: pubkey,
|
|
2685
|
-
addedAt: 0,
|
|
2686
|
-
addedInEpoch: EpochId("epoch-0")
|
|
2687
|
-
},
|
|
2688
|
-
removedAt: Date.now(),
|
|
2689
|
-
removedInEpoch: result.epoch.id
|
|
3030
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2690
3031
|
});
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
3032
|
+
}
|
|
3033
|
+
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
3034
|
+
const ensureOpen = (effect) => withLog(
|
|
3035
|
+
Effect21.gen(function* () {
|
|
3036
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3037
|
+
return yield* new StorageError({ message: "Database is closed" });
|
|
3038
|
+
}
|
|
3039
|
+
return yield* effect;
|
|
3040
|
+
})
|
|
3041
|
+
);
|
|
3042
|
+
const ensureSyncOpen = (effect) => withLog(
|
|
3043
|
+
Effect21.gen(function* () {
|
|
3044
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3045
|
+
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
3046
|
+
}
|
|
3047
|
+
return yield* effect;
|
|
3048
|
+
})
|
|
3049
|
+
);
|
|
3050
|
+
const dbHandle = {
|
|
3051
|
+
collection: (name) => {
|
|
3052
|
+
const handle = handles.get(name);
|
|
3053
|
+
if (!handle) throw new Error(`Collection "${name}" not found in schema`);
|
|
3054
|
+
return handle;
|
|
3055
|
+
},
|
|
3056
|
+
publicKey: identity.publicKey,
|
|
3057
|
+
members: handles.get("_members"),
|
|
3058
|
+
exportKey: () => identity.exportKey(),
|
|
3059
|
+
exportInvite: () => ({
|
|
3060
|
+
epochKeys: [...exportEpochKeys(epochStore)],
|
|
3061
|
+
relays: [...config.relays],
|
|
3062
|
+
dbName: config.dbName
|
|
3063
|
+
}),
|
|
3064
|
+
close: () => withLog(
|
|
3065
|
+
Effect21.gen(function* () {
|
|
3066
|
+
if (yield* Ref5.get(closedRef)) return;
|
|
3067
|
+
yield* Ref5.set(closedRef, true);
|
|
3068
|
+
syncHandle.stopHealing();
|
|
3069
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3070
|
+
})
|
|
3071
|
+
),
|
|
3072
|
+
rebuild: () => ensureOpen(
|
|
3073
|
+
rebuild(
|
|
3074
|
+
storage,
|
|
3075
|
+
allSchemaEntries.map(([, def]) => def.name)
|
|
3076
|
+
)
|
|
3077
|
+
),
|
|
3078
|
+
sync: () => ensureSyncOpen(
|
|
3079
|
+
syncHandle.sync().pipe(
|
|
3080
|
+
Effect21.tap(
|
|
3081
|
+
() => Effect21.sync(() => {
|
|
3082
|
+
syncHandle.startHealing();
|
|
3083
|
+
})
|
|
3084
|
+
)
|
|
3085
|
+
)
|
|
3086
|
+
),
|
|
3087
|
+
getSyncStatus: () => syncStatus.get(),
|
|
3088
|
+
subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
|
|
3089
|
+
pendingCount: () => publishQueue.size(),
|
|
3090
|
+
subscribePendingCount: (callback) => publishQueue.subscribe(callback),
|
|
3091
|
+
getRelayStatus: () => relay.getStatus(),
|
|
3092
|
+
subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
|
|
3093
|
+
addMember: (pubkey) => ensureOpen(
|
|
3094
|
+
Effect21.gen(function* () {
|
|
3095
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
3096
|
+
if (existing && !existing.removedAt) return;
|
|
3097
|
+
yield* putMemberRecord({
|
|
3098
|
+
id: pubkey,
|
|
3099
|
+
addedAt: Date.now(),
|
|
3100
|
+
addedInEpoch: getCurrentEpoch(epochStore).id,
|
|
3101
|
+
...existing ? { removedAt: void 0, removedInEpoch: void 0 } : {}
|
|
3102
|
+
});
|
|
3103
|
+
})
|
|
3104
|
+
),
|
|
3105
|
+
removeMember: (pubkey) => ensureOpen(
|
|
3106
|
+
Effect21.gen(function* () {
|
|
3107
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
3108
|
+
const activeMembers = allMembers.filter(
|
|
3109
|
+
(member) => !member.removedAt && member.id !== pubkey
|
|
3110
|
+
);
|
|
3111
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
3112
|
+
const result = createRotation(
|
|
3113
|
+
epochStore,
|
|
3114
|
+
identity.privateKey,
|
|
3115
|
+
identity.publicKey,
|
|
3116
|
+
activePubkeys,
|
|
3117
|
+
[pubkey]
|
|
3118
|
+
);
|
|
3119
|
+
addEpoch(epochStore, result.epoch);
|
|
3120
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
3121
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
3122
|
+
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
3123
|
+
yield* putMemberRecord({
|
|
3124
|
+
...memberRecord ?? {
|
|
3125
|
+
id: pubkey,
|
|
3126
|
+
addedAt: 0,
|
|
3127
|
+
addedInEpoch: EpochId("epoch-0")
|
|
3128
|
+
},
|
|
3129
|
+
removedAt: Date.now(),
|
|
3130
|
+
removedInEpoch: result.epoch.id
|
|
3131
|
+
});
|
|
3132
|
+
yield* Effect21.forEach(
|
|
3133
|
+
result.wrappedEvents,
|
|
3134
|
+
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
3135
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3136
|
+
Effect21.ignore
|
|
3137
|
+
),
|
|
3138
|
+
{ discard: true }
|
|
3139
|
+
);
|
|
3140
|
+
yield* Effect21.forEach(
|
|
3141
|
+
result.removalNotices,
|
|
3142
|
+
(notice) => relay.publish(notice, [...config.relays]).pipe(
|
|
3143
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3144
|
+
Effect21.ignore
|
|
3145
|
+
),
|
|
3146
|
+
{ discard: true }
|
|
3147
|
+
);
|
|
3148
|
+
yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
|
|
3149
|
+
})
|
|
3150
|
+
),
|
|
3151
|
+
getMembers: () => ensureOpen(
|
|
3152
|
+
Effect21.gen(function* () {
|
|
3153
|
+
const allRecords = yield* storage.getAllRecords("_members");
|
|
3154
|
+
return allRecords.filter((record) => !record._d).map(mapMemberRecord);
|
|
3155
|
+
})
|
|
3156
|
+
),
|
|
3157
|
+
getProfile: () => ensureOpen(
|
|
3158
|
+
Effect21.gen(function* () {
|
|
3159
|
+
const record = yield* storage.getRecord("_members", identity.publicKey);
|
|
3160
|
+
if (!record) return {};
|
|
3161
|
+
const profile = {};
|
|
3162
|
+
if (record.name !== void 0) profile.name = record.name;
|
|
3163
|
+
if (record.picture !== void 0) profile.picture = record.picture;
|
|
3164
|
+
if (record.about !== void 0) profile.about = record.about;
|
|
3165
|
+
if (record.nip05 !== void 0) profile.nip05 = record.nip05;
|
|
3166
|
+
return profile;
|
|
3167
|
+
})
|
|
3168
|
+
),
|
|
3169
|
+
setProfile: (profile) => ensureOpen(
|
|
3170
|
+
Effect21.gen(function* () {
|
|
3171
|
+
const existing = yield* storage.getRecord("_members", identity.publicKey);
|
|
3172
|
+
if (!existing) {
|
|
3173
|
+
return yield* new ValidationError({ message: "Current user is not a member" });
|
|
3174
|
+
}
|
|
3175
|
+
const { _d, _u, _a, _e, ...memberFields } = existing;
|
|
3176
|
+
yield* putMemberRecord({ ...memberFields, ...profile });
|
|
3177
|
+
})
|
|
3178
|
+
)
|
|
3179
|
+
};
|
|
3180
|
+
return dbHandle;
|
|
3181
|
+
}).pipe(Effect21.withLogSpan("tablinum.init"))
|
|
3182
|
+
).pipe(Layer9.provide(AllServicesLive));
|
|
2725
3183
|
|
|
2726
3184
|
// src/db/create-tablinum.ts
|
|
2727
3185
|
function resolveLogLevel(input) {
|
|
2728
|
-
if (input ===
|
|
2729
|
-
return "None";
|
|
3186
|
+
if (input === void 0 || input === "none") return "None";
|
|
2730
3187
|
switch (input) {
|
|
2731
3188
|
case "debug":
|
|
2732
3189
|
return "Debug";
|
|
@@ -2786,14 +3243,12 @@ function createDeferred() {
|
|
|
2786
3243
|
promise,
|
|
2787
3244
|
settled: () => settled,
|
|
2788
3245
|
resolve: (value) => {
|
|
2789
|
-
if (settled)
|
|
2790
|
-
return;
|
|
3246
|
+
if (settled) return;
|
|
2791
3247
|
settled = true;
|
|
2792
3248
|
resolvePromise(value);
|
|
2793
3249
|
},
|
|
2794
3250
|
reject: (reason) => {
|
|
2795
|
-
if (settled)
|
|
2796
|
-
return;
|
|
3251
|
+
if (settled) return;
|
|
2797
3252
|
settled = true;
|
|
2798
3253
|
rejectPromise(reason);
|
|
2799
3254
|
}
|
|
@@ -2815,7 +3270,9 @@ function wrapQueryBuilder(getBuilder, touchVersion, ready) {
|
|
|
2815
3270
|
},
|
|
2816
3271
|
first: () => {
|
|
2817
3272
|
touchVersion();
|
|
2818
|
-
return ready.then(
|
|
3273
|
+
return ready.then(
|
|
3274
|
+
() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull))
|
|
3275
|
+
);
|
|
2819
3276
|
},
|
|
2820
3277
|
count: () => {
|
|
2821
3278
|
touchVersion();
|
|
@@ -2847,7 +3304,9 @@ function wrapOrderByBuilder(getBuilder, touchVersion, ready) {
|
|
|
2847
3304
|
},
|
|
2848
3305
|
first: () => {
|
|
2849
3306
|
touchVersion();
|
|
2850
|
-
return ready.then(
|
|
3307
|
+
return ready.then(
|
|
3308
|
+
() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull))
|
|
3309
|
+
);
|
|
2851
3310
|
},
|
|
2852
3311
|
count: () => {
|
|
2853
3312
|
touchVersion();
|
|
@@ -2857,7 +3316,7 @@ function wrapOrderByBuilder(getBuilder, touchVersion, ready) {
|
|
|
2857
3316
|
}
|
|
2858
3317
|
|
|
2859
3318
|
// src/svelte/collection.svelte.ts
|
|
2860
|
-
|
|
3319
|
+
var Collection = class {
|
|
2861
3320
|
error = $state(null);
|
|
2862
3321
|
#handle = null;
|
|
2863
3322
|
#ready = createDeferred();
|
|
@@ -2865,15 +3324,16 @@ class Collection {
|
|
|
2865
3324
|
#watchAbort = null;
|
|
2866
3325
|
#watchFiber = null;
|
|
2867
3326
|
#logLevel = "None";
|
|
3327
|
+
/** @internal */
|
|
2868
3328
|
_bind(handle, logLevel = "None") {
|
|
2869
|
-
if (this.#handle)
|
|
2870
|
-
return;
|
|
3329
|
+
if (this.#handle) return;
|
|
2871
3330
|
this.#handle = handle;
|
|
2872
3331
|
this.#logLevel = logLevel;
|
|
2873
3332
|
this.error = null;
|
|
2874
3333
|
this.#settleReady();
|
|
2875
3334
|
this.#startWatch();
|
|
2876
3335
|
}
|
|
3336
|
+
/** @internal */
|
|
2877
3337
|
_fail(err) {
|
|
2878
3338
|
this.error = err;
|
|
2879
3339
|
this.#settleReady(err);
|
|
@@ -2886,31 +3346,41 @@ class Collection {
|
|
|
2886
3346
|
}
|
|
2887
3347
|
}
|
|
2888
3348
|
#startWatch() {
|
|
2889
|
-
if (!this.#handle)
|
|
2890
|
-
|
|
2891
|
-
const abort = new AbortController;
|
|
3349
|
+
if (!this.#handle) return;
|
|
3350
|
+
const abort = new AbortController();
|
|
2892
3351
|
this.#watchAbort = abort;
|
|
2893
|
-
this.#watchFiber = Effect24.runFork(
|
|
2894
|
-
|
|
2895
|
-
this.#
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3352
|
+
this.#watchFiber = Effect24.runFork(
|
|
3353
|
+
Stream4.runForEach(
|
|
3354
|
+
this.#handle.watch(),
|
|
3355
|
+
(_records) => Effect24.sync(() => {
|
|
3356
|
+
if (!abort.signal.aborted) {
|
|
3357
|
+
this.#version++;
|
|
3358
|
+
}
|
|
3359
|
+
})
|
|
3360
|
+
).pipe(
|
|
3361
|
+
Effect24.catch(
|
|
3362
|
+
(e) => Effect24.sync(() => {
|
|
3363
|
+
if (!abort.signal.aborted) {
|
|
3364
|
+
this.error = e instanceof Error ? e : new Error(String(e));
|
|
3365
|
+
}
|
|
3366
|
+
})
|
|
3367
|
+
),
|
|
3368
|
+
Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)
|
|
3369
|
+
)
|
|
3370
|
+
);
|
|
2902
3371
|
}
|
|
2903
3372
|
#touchVersion = () => {
|
|
2904
|
-
this.#version;
|
|
3373
|
+
void this.#version;
|
|
2905
3374
|
};
|
|
2906
3375
|
#handleOrThrow = () => {
|
|
2907
|
-
if (this.#handle)
|
|
2908
|
-
return this.#handle;
|
|
3376
|
+
if (this.#handle) return this.#handle;
|
|
2909
3377
|
throw this.error ?? new ClosedError({ message: "Collection is not ready" });
|
|
2910
3378
|
};
|
|
2911
3379
|
#run = async (getEffect) => {
|
|
2912
3380
|
await this.#ready.promise;
|
|
2913
|
-
return Effect24.runPromise(
|
|
3381
|
+
return Effect24.runPromise(
|
|
3382
|
+
getEffect().pipe(Effect24.provideService(References5.MinimumLogLevel, this.#logLevel))
|
|
3383
|
+
);
|
|
2914
3384
|
};
|
|
2915
3385
|
add = (data) => {
|
|
2916
3386
|
return this.#run(() => this.#handleOrThrow().add(data));
|
|
@@ -2929,7 +3399,11 @@ class Collection {
|
|
|
2929
3399
|
return this.#run(() => this.#handleOrThrow().get(id));
|
|
2930
3400
|
}
|
|
2931
3401
|
this.#touchVersion();
|
|
2932
|
-
return this.#run(
|
|
3402
|
+
return this.#run(
|
|
3403
|
+
() => Stream4.runHead(this.#handleOrThrow().watch()).pipe(
|
|
3404
|
+
Effect24.map((opt) => Option11.getOrElse(opt, () => []))
|
|
3405
|
+
)
|
|
3406
|
+
);
|
|
2933
3407
|
}
|
|
2934
3408
|
first = () => {
|
|
2935
3409
|
this.#touchVersion();
|
|
@@ -2940,11 +3414,20 @@ class Collection {
|
|
|
2940
3414
|
return this.#run(() => this.#handleOrThrow().count());
|
|
2941
3415
|
};
|
|
2942
3416
|
where = (field2) => {
|
|
2943
|
-
return wrapWhereClause(
|
|
3417
|
+
return wrapWhereClause(
|
|
3418
|
+
() => this.#handleOrThrow().where(field2),
|
|
3419
|
+
this.#touchVersion,
|
|
3420
|
+
this.#ready.promise
|
|
3421
|
+
);
|
|
2944
3422
|
};
|
|
2945
3423
|
orderBy = (field2) => {
|
|
2946
|
-
return wrapOrderByBuilder(
|
|
3424
|
+
return wrapOrderByBuilder(
|
|
3425
|
+
() => this.#handleOrThrow().orderBy(field2),
|
|
3426
|
+
this.#touchVersion,
|
|
3427
|
+
this.#ready.promise
|
|
3428
|
+
);
|
|
2947
3429
|
};
|
|
3430
|
+
/** @internal */
|
|
2948
3431
|
_destroy(reason = new ClosedError({ message: "Collection is closed" })) {
|
|
2949
3432
|
if (this.#watchAbort) {
|
|
2950
3433
|
this.#watchAbort.abort();
|
|
@@ -2958,10 +3441,10 @@ class Collection {
|
|
|
2958
3441
|
this.error ??= reason;
|
|
2959
3442
|
this.#settleReady(this.error);
|
|
2960
3443
|
}
|
|
2961
|
-
}
|
|
3444
|
+
};
|
|
2962
3445
|
|
|
2963
3446
|
// src/svelte/tablinum.svelte.ts
|
|
2964
|
-
|
|
3447
|
+
var Tablinum2 = class {
|
|
2965
3448
|
status = $state("initializing");
|
|
2966
3449
|
syncStatus = $state("idle");
|
|
2967
3450
|
pendingCount = $state(0);
|
|
@@ -2970,8 +3453,8 @@ class Tablinum2 {
|
|
|
2970
3453
|
ready;
|
|
2971
3454
|
#handle = null;
|
|
2972
3455
|
#scope = null;
|
|
2973
|
-
#collections = new Map;
|
|
2974
|
-
#members = new Collection;
|
|
3456
|
+
#collections = /* @__PURE__ */ new Map();
|
|
3457
|
+
#members = new Collection();
|
|
2975
3458
|
#unsubscribeSyncStatus = null;
|
|
2976
3459
|
#unsubscribePendingCount = null;
|
|
2977
3460
|
#unsubscribeRelayStatus = null;
|
|
@@ -3000,16 +3483,20 @@ class Tablinum2 {
|
|
|
3000
3483
|
const handle = this.#requireReady();
|
|
3001
3484
|
try {
|
|
3002
3485
|
this.error = null;
|
|
3003
|
-
return await Effect25.runPromise(
|
|
3486
|
+
return await Effect25.runPromise(
|
|
3487
|
+
run(handle).pipe(Effect25.provideService(References6.MinimumLogLevel, this.#logLevel))
|
|
3488
|
+
);
|
|
3004
3489
|
} catch (e) {
|
|
3005
3490
|
this.error = e instanceof Error ? e : new Error(String(e));
|
|
3006
3491
|
throw this.error;
|
|
3007
3492
|
}
|
|
3008
3493
|
};
|
|
3009
|
-
async#init(config) {
|
|
3494
|
+
async #init(config) {
|
|
3010
3495
|
const scope = Effect25.runSync(Scope6.make());
|
|
3011
3496
|
try {
|
|
3012
|
-
const handle = await Effect25.runPromise(
|
|
3497
|
+
const handle = await Effect25.runPromise(
|
|
3498
|
+
createTablinum(config).pipe(Effect25.provideService(Scope6.Scope, scope))
|
|
3499
|
+
);
|
|
3013
3500
|
if (this.#closed) {
|
|
3014
3501
|
await Effect25.runPromise(Scope6.close(scope, Exit2.void));
|
|
3015
3502
|
this.#scope = null;
|
|
@@ -3032,7 +3519,8 @@ class Tablinum2 {
|
|
|
3032
3519
|
this.status = "ready";
|
|
3033
3520
|
this.#settleReady();
|
|
3034
3521
|
} catch (e) {
|
|
3035
|
-
await Effect25.runPromise(Scope6.close(scope, Exit2.fail(e))).catch(() => {
|
|
3522
|
+
await Effect25.runPromise(Scope6.close(scope, Exit2.fail(e))).catch(() => {
|
|
3523
|
+
});
|
|
3036
3524
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
3037
3525
|
this.error = err;
|
|
3038
3526
|
this.status = "error";
|
|
@@ -3055,7 +3543,7 @@ class Tablinum2 {
|
|
|
3055
3543
|
}
|
|
3056
3544
|
let col = this.#collections.get(name);
|
|
3057
3545
|
if (!col) {
|
|
3058
|
-
col = new Collection;
|
|
3546
|
+
col = new Collection();
|
|
3059
3547
|
this.#collections.set(name, col);
|
|
3060
3548
|
if (this.#handle) {
|
|
3061
3549
|
col._bind(this.#handle.collection(name), this.#logLevel);
|
|
@@ -3076,8 +3564,7 @@ class Tablinum2 {
|
|
|
3076
3564
|
return this.#requireReady().exportInvite();
|
|
3077
3565
|
}
|
|
3078
3566
|
close = async () => {
|
|
3079
|
-
if (this.#closed)
|
|
3080
|
-
return;
|
|
3567
|
+
if (this.#closed) return;
|
|
3081
3568
|
this.#closed = true;
|
|
3082
3569
|
const closeError = new ClosedError({
|
|
3083
3570
|
message: this.#readyState.settled() ? "Tablinum is closed" : "Tablinum was closed before initialization completed"
|
|
@@ -3095,8 +3582,7 @@ class Tablinum2 {
|
|
|
3095
3582
|
this.#unsubscribeRelayStatus = null;
|
|
3096
3583
|
}
|
|
3097
3584
|
this.#members._destroy(closeError);
|
|
3098
|
-
for (const col of this.#collections.values())
|
|
3099
|
-
col._destroy(closeError);
|
|
3585
|
+
for (const col of this.#collections.values()) col._destroy(closeError);
|
|
3100
3586
|
this.#collections.clear();
|
|
3101
3587
|
const handle = this.#handle;
|
|
3102
3588
|
const scope = this.#scope;
|
|
@@ -3119,19 +3605,19 @@ class Tablinum2 {
|
|
|
3119
3605
|
getMembers = async () => this.#runHandleEffect((handle) => handle.getMembers());
|
|
3120
3606
|
getProfile = async () => this.#runHandleEffect((handle) => handle.getProfile());
|
|
3121
3607
|
setProfile = async (profile) => this.#runHandleEffect((handle) => handle.setProfile(profile));
|
|
3122
|
-
}
|
|
3608
|
+
};
|
|
3123
3609
|
export {
|
|
3124
|
-
|
|
3125
|
-
encodeInvite,
|
|
3126
|
-
decodeInvite,
|
|
3127
|
-
collection,
|
|
3128
|
-
ValidationError,
|
|
3129
|
-
Tablinum2 as Tablinum,
|
|
3130
|
-
SyncError,
|
|
3131
|
-
StorageError,
|
|
3132
|
-
RelayError,
|
|
3133
|
-
NotFoundError,
|
|
3134
|
-
CryptoError,
|
|
3610
|
+
ClosedError,
|
|
3135
3611
|
Collection,
|
|
3136
|
-
|
|
3612
|
+
CryptoError,
|
|
3613
|
+
NotFoundError,
|
|
3614
|
+
RelayError,
|
|
3615
|
+
StorageError,
|
|
3616
|
+
SyncError,
|
|
3617
|
+
Tablinum2 as Tablinum,
|
|
3618
|
+
ValidationError,
|
|
3619
|
+
collection,
|
|
3620
|
+
decodeInvite,
|
|
3621
|
+
encodeInvite,
|
|
3622
|
+
field
|
|
3137
3623
|
};
|