tablinum 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1330 -910
- package/dist/storage/idb.d.ts +0 -1
- package/dist/svelte/index.svelte.js +1399 -948
- package/dist/sync/compact-event.d.ts +3 -0
- 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
|
});
|
|
@@ -442,9 +465,16 @@ function buildStructSchema(def, options = {}) {
|
|
|
442
465
|
}
|
|
443
466
|
function buildValidator(collectionName, def) {
|
|
444
467
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
445
|
-
return (input) => decode(input).pipe(
|
|
446
|
-
|
|
447
|
-
|
|
468
|
+
return (input) => decode(input).pipe(
|
|
469
|
+
Effect4.map(
|
|
470
|
+
(result) => result
|
|
471
|
+
),
|
|
472
|
+
Effect4.mapError(
|
|
473
|
+
(e) => new ValidationError({
|
|
474
|
+
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
475
|
+
})
|
|
476
|
+
)
|
|
477
|
+
);
|
|
448
478
|
}
|
|
449
479
|
function buildPartialValidator(collectionName, def) {
|
|
450
480
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
@@ -456,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
|
}
|
|
@@ -1398,8 +1466,7 @@ 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
|
-
break;
|
|
1469
|
+
if (response.msgHex === null) break;
|
|
1403
1470
|
const reconcileResult = yield* Effect7.try({
|
|
1404
1471
|
try: () => neg.reconcile(response.msgHex),
|
|
1405
1472
|
catch: (e) => new SyncError({
|
|
@@ -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,65 +1580,79 @@ 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
1623
|
const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1549
1624
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1550
|
-
if (existing)
|
|
1551
|
-
return null;
|
|
1625
|
+
if (existing) return null;
|
|
1552
1626
|
const unwrapResult = yield* Effect8.result(giftWrapHandle.unwrap(remoteGw));
|
|
1553
1627
|
if (unwrapResult._tag === "Failure") {
|
|
1554
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1628
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1555
1629
|
return null;
|
|
1556
1630
|
}
|
|
1557
1631
|
const rumor = unwrapResult.success;
|
|
1558
1632
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1559
1633
|
if (!dTag) {
|
|
1560
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1634
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1561
1635
|
return null;
|
|
1562
1636
|
}
|
|
1563
1637
|
const colonIdx = dTag.indexOf(":");
|
|
1564
1638
|
if (colonIdx === -1) {
|
|
1565
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1639
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1566
1640
|
return null;
|
|
1567
1641
|
}
|
|
1568
1642
|
const collectionName = dTag.substring(0, colonIdx);
|
|
1569
1643
|
const recordId = dTag.substring(colonIdx + 1);
|
|
1570
1644
|
const retention = knownCollections.get(collectionName);
|
|
1571
|
-
if (retention ===
|
|
1572
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1645
|
+
if (retention === void 0) {
|
|
1646
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1573
1647
|
return null;
|
|
1574
1648
|
}
|
|
1575
1649
|
if (rumor.pubkey) {
|
|
1576
1650
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1577
1651
|
if (reject) {
|
|
1578
|
-
yield* Effect8.logWarning("Rejected write from removed member", {
|
|
1579
|
-
|
|
1652
|
+
yield* Effect8.logWarning("Rejected write from removed member", {
|
|
1653
|
+
author: rumor.pubkey.slice(0, 12)
|
|
1654
|
+
});
|
|
1655
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1580
1656
|
return null;
|
|
1581
1657
|
}
|
|
1582
1658
|
}
|
|
@@ -1584,14 +1660,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1584
1660
|
let kind = "u";
|
|
1585
1661
|
const parsed = yield* Effect8.try({
|
|
1586
1662
|
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 });
|
|
1663
|
+
catch: () => void 0
|
|
1664
|
+
}).pipe(Effect8.orElseSucceed(() => void 0));
|
|
1665
|
+
if (parsed === void 0) {
|
|
1666
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1595
1667
|
return null;
|
|
1596
1668
|
}
|
|
1597
1669
|
if (parsed === null || parsed._deleted) {
|
|
@@ -1599,18 +1671,19 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1599
1671
|
} else {
|
|
1600
1672
|
data = parsed;
|
|
1601
1673
|
}
|
|
1602
|
-
const author = rumor.pubkey ||
|
|
1674
|
+
const author = rumor.pubkey || void 0;
|
|
1603
1675
|
const event = {
|
|
1604
1676
|
id: rumor.id,
|
|
1605
1677
|
collection: collectionName,
|
|
1606
1678
|
recordId,
|
|
1607
1679
|
kind,
|
|
1608
1680
|
data,
|
|
1609
|
-
createdAt: rumor.created_at *
|
|
1681
|
+
createdAt: rumor.created_at * 1e3,
|
|
1610
1682
|
author
|
|
1611
1683
|
};
|
|
1612
1684
|
yield* storage.putGiftWrap({
|
|
1613
1685
|
id: remoteGw.id,
|
|
1686
|
+
event: remoteGw,
|
|
1614
1687
|
createdAt: remoteGw.created_at
|
|
1615
1688
|
});
|
|
1616
1689
|
yield* storage.putEvent(event);
|
|
@@ -1618,7 +1691,12 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1618
1691
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1619
1692
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1620
1693
|
}
|
|
1621
|
-
yield* Effect8.logDebug("Processed gift wrap", {
|
|
1694
|
+
yield* Effect8.logDebug("Processed gift wrap", {
|
|
1695
|
+
collection: collectionName,
|
|
1696
|
+
recordId,
|
|
1697
|
+
kind,
|
|
1698
|
+
author: author?.slice(0, 12)
|
|
1699
|
+
});
|
|
1622
1700
|
if (author && onNewAuthor) {
|
|
1623
1701
|
onNewAuthor(author);
|
|
1624
1702
|
}
|
|
@@ -1631,32 +1709,34 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1631
1709
|
}
|
|
1632
1710
|
});
|
|
1633
1711
|
const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1634
|
-
const unwrapResult = yield* Effect8.result(
|
|
1635
|
-
try
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1712
|
+
const unwrapResult = yield* Effect8.result(
|
|
1713
|
+
Effect8.try({
|
|
1714
|
+
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1715
|
+
catch: (e) => new CryptoError({
|
|
1716
|
+
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
1717
|
+
cause: e
|
|
1718
|
+
})
|
|
1639
1719
|
})
|
|
1640
|
-
|
|
1641
|
-
if (unwrapResult._tag === "Failure")
|
|
1642
|
-
return false;
|
|
1720
|
+
);
|
|
1721
|
+
if (unwrapResult._tag === "Failure") return false;
|
|
1643
1722
|
const rumor = unwrapResult.success;
|
|
1644
1723
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1645
|
-
if (!dTag)
|
|
1646
|
-
return false;
|
|
1724
|
+
if (!dTag) return false;
|
|
1647
1725
|
const removalNoticeOpt = parseRemovalNotice(rumor.content, dTag);
|
|
1648
1726
|
if (Option5.isSome(removalNoticeOpt)) {
|
|
1649
|
-
if (onRemoved)
|
|
1650
|
-
onRemoved(removalNoticeOpt.value);
|
|
1727
|
+
if (onRemoved) onRemoved(removalNoticeOpt.value);
|
|
1651
1728
|
return true;
|
|
1652
1729
|
}
|
|
1653
1730
|
const rotationDataOpt = parseRotationEvent(rumor.content, dTag);
|
|
1654
|
-
if (Option5.isNone(rotationDataOpt))
|
|
1655
|
-
return false;
|
|
1731
|
+
if (Option5.isNone(rotationDataOpt)) return false;
|
|
1656
1732
|
const rotationData = rotationDataOpt.value;
|
|
1657
|
-
if (epochStore.epochs.has(rotationData.epochId))
|
|
1658
|
-
|
|
1659
|
-
|
|
1733
|
+
if (epochStore.epochs.has(rotationData.epochId)) return false;
|
|
1734
|
+
const epoch = createEpochKey(
|
|
1735
|
+
rotationData.epochId,
|
|
1736
|
+
rotationData.epochKey,
|
|
1737
|
+
rumor.pubkey || "",
|
|
1738
|
+
rotationData.parentEpoch
|
|
1739
|
+
);
|
|
1660
1740
|
addEpoch(epochStore, epoch);
|
|
1661
1741
|
epochStore.currentEpochId = epoch.id;
|
|
1662
1742
|
let membersChanged = false;
|
|
@@ -1676,79 +1756,158 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1676
1756
|
membersChanged = true;
|
|
1677
1757
|
}
|
|
1678
1758
|
}
|
|
1679
|
-
if (membersChanged && onMembersChanged)
|
|
1680
|
-
onMembersChanged();
|
|
1759
|
+
if (membersChanged && onMembersChanged) onMembersChanged();
|
|
1681
1760
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1682
1761
|
return true;
|
|
1683
1762
|
});
|
|
1684
|
-
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1763
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1764
|
+
relayUrls,
|
|
1765
|
+
(url) => Effect8.gen(function* () {
|
|
1766
|
+
yield* relay.subscribe(filter, url, (event) => {
|
|
1767
|
+
forkHandled(onEvent(event));
|
|
1768
|
+
}).pipe(
|
|
1769
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1770
|
+
Effect8.ignore
|
|
1771
|
+
);
|
|
1772
|
+
}),
|
|
1773
|
+
{ discard: true }
|
|
1774
|
+
);
|
|
1689
1775
|
const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1690
1776
|
yield* Effect8.logDebug("Syncing relay", { relay: url });
|
|
1691
|
-
const reconcileResult = yield* Effect8.result(
|
|
1777
|
+
const reconcileResult = yield* Effect8.result(
|
|
1778
|
+
reconcileWithRelay(storage, relay, url, Array.from(pubKeys))
|
|
1779
|
+
);
|
|
1692
1780
|
if (reconcileResult._tag === "Failure") {
|
|
1693
1781
|
onSyncError?.(reconcileResult.failure);
|
|
1694
1782
|
return;
|
|
1695
1783
|
}
|
|
1696
1784
|
const { haveIds, needIds } = reconcileResult.success;
|
|
1697
|
-
yield* Effect8.logDebug("Relay reconciliation result", {
|
|
1785
|
+
yield* Effect8.logDebug("Relay reconciliation result", {
|
|
1786
|
+
relay: url,
|
|
1787
|
+
need: needIds.length,
|
|
1788
|
+
have: haveIds.length
|
|
1789
|
+
});
|
|
1698
1790
|
if (needIds.length > 0) {
|
|
1699
|
-
const fetched = yield* relay.fetchEvents(needIds, url).pipe(
|
|
1791
|
+
const fetched = yield* relay.fetchEvents(needIds, url).pipe(
|
|
1792
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1793
|
+
Effect8.orElseSucceed(() => [])
|
|
1794
|
+
);
|
|
1700
1795
|
const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
|
|
1701
|
-
yield* Effect8.forEach(
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1796
|
+
yield* Effect8.forEach(
|
|
1797
|
+
sorted,
|
|
1798
|
+
(remoteGw) => Effect8.gen(function* () {
|
|
1799
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(
|
|
1800
|
+
Effect8.orElseSucceed(() => null)
|
|
1801
|
+
);
|
|
1802
|
+
if (collection2) changedCollections.add(collection2);
|
|
1803
|
+
}),
|
|
1804
|
+
{ discard: true }
|
|
1805
|
+
);
|
|
1706
1806
|
}
|
|
1707
1807
|
if (haveIds.length > 0) {
|
|
1708
|
-
yield* Effect8.forEach(
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1808
|
+
yield* Effect8.forEach(
|
|
1809
|
+
haveIds,
|
|
1810
|
+
(id) => Effect8.gen(function* () {
|
|
1811
|
+
const gw = yield* storage.getGiftWrap(id);
|
|
1812
|
+
if (!gw?.event) return;
|
|
1813
|
+
yield* relay.publish(gw.event, [url]).pipe(
|
|
1814
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1815
|
+
Effect8.ignore
|
|
1816
|
+
);
|
|
1817
|
+
}),
|
|
1818
|
+
{ discard: true }
|
|
1819
|
+
);
|
|
1714
1820
|
}
|
|
1715
1821
|
}).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
|
|
1822
|
+
let healingActive = false;
|
|
1823
|
+
const healingEffect = Effect8.gen(function* () {
|
|
1824
|
+
if (!healingActive) return;
|
|
1825
|
+
const status = yield* syncStatus.get();
|
|
1826
|
+
if (status === "syncing") return;
|
|
1827
|
+
yield* syncStatus.set("syncing");
|
|
1828
|
+
yield* Effect8.gen(function* () {
|
|
1829
|
+
const pubKeys = getSubscriptionPubKeys();
|
|
1830
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1831
|
+
yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
|
|
1832
|
+
discard: true
|
|
1833
|
+
});
|
|
1834
|
+
if (changedCollections.size > 0) {
|
|
1835
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1836
|
+
}
|
|
1837
|
+
}).pipe(Effect8.ensuring(syncStatus.set("idle")));
|
|
1838
|
+
}).pipe(Effect8.ignore);
|
|
1716
1839
|
const handle = {
|
|
1717
1840
|
sync: () => Effect8.gen(function* () {
|
|
1718
1841
|
yield* Effect8.logInfo("Sync started");
|
|
1719
1842
|
yield* syncStatus.set("syncing");
|
|
1720
1843
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1721
|
-
const changedCollections = new Set;
|
|
1844
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1722
1845
|
yield* Effect8.gen(function* () {
|
|
1723
1846
|
const pubKeys = getSubscriptionPubKeys();
|
|
1724
1847
|
yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
|
|
1725
1848
|
discard: true
|
|
1726
1849
|
});
|
|
1727
1850
|
yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
|
|
1728
|
-
}).pipe(
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1851
|
+
}).pipe(
|
|
1852
|
+
Effect8.ensuring(
|
|
1853
|
+
Effect8.gen(function* () {
|
|
1854
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1855
|
+
yield* syncStatus.set("idle");
|
|
1856
|
+
})
|
|
1857
|
+
)
|
|
1858
|
+
);
|
|
1732
1859
|
yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
1733
1860
|
}).pipe(Effect8.withLogSpan("tablinum.sync")),
|
|
1734
1861
|
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1735
|
-
if (!giftWrap.event)
|
|
1736
|
-
|
|
1737
|
-
|
|
1862
|
+
if (!giftWrap.event) return;
|
|
1863
|
+
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1864
|
+
Effect8.tapError(
|
|
1865
|
+
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1866
|
+
Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
1867
|
+
Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
|
|
1868
|
+
)
|
|
1869
|
+
),
|
|
1870
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1871
|
+
Effect8.ignore
|
|
1872
|
+
);
|
|
1738
1873
|
}),
|
|
1739
1874
|
startSubscription: () => Effect8.gen(function* () {
|
|
1740
1875
|
const pubKeys = getSubscriptionPubKeys();
|
|
1741
1876
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1742
1877
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1743
|
-
yield* subscribeAcrossRelays(
|
|
1878
|
+
yield* subscribeAcrossRelays(
|
|
1879
|
+
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1880
|
+
(event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
|
|
1881
|
+
);
|
|
1744
1882
|
}
|
|
1745
1883
|
}),
|
|
1746
|
-
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap)
|
|
1884
|
+
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap),
|
|
1885
|
+
startHealing: () => {
|
|
1886
|
+
if (healingActive) return;
|
|
1887
|
+
healingActive = true;
|
|
1888
|
+
forkHandled(
|
|
1889
|
+
Effect8.sleep(Duration.minutes(5)).pipe(
|
|
1890
|
+
Effect8.andThen(healingEffect),
|
|
1891
|
+
Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
1892
|
+
Effect8.ensuring(Effect8.sync(() => {
|
|
1893
|
+
healingActive = false;
|
|
1894
|
+
}))
|
|
1895
|
+
)
|
|
1896
|
+
);
|
|
1897
|
+
},
|
|
1898
|
+
stopHealing: () => {
|
|
1899
|
+
healingActive = false;
|
|
1900
|
+
}
|
|
1747
1901
|
};
|
|
1748
|
-
forkHandled(
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1902
|
+
forkHandled(
|
|
1903
|
+
publishQueue.size().pipe(
|
|
1904
|
+
Effect8.flatMap(
|
|
1905
|
+
(size) => Effect8.sync(() => {
|
|
1906
|
+
if (size > 0) scheduleAutoFlush();
|
|
1907
|
+
})
|
|
1908
|
+
)
|
|
1909
|
+
)
|
|
1910
|
+
);
|
|
1752
1911
|
return handle;
|
|
1753
1912
|
}
|
|
1754
1913
|
|
|
@@ -1804,9 +1963,14 @@ var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(Aut
|
|
|
1804
1963
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1805
1964
|
return Effect9.gen(function* () {
|
|
1806
1965
|
for (const url of relayUrls) {
|
|
1807
|
-
const result = yield* Effect9.result(
|
|
1966
|
+
const result = yield* Effect9.result(
|
|
1967
|
+
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
1968
|
+
);
|
|
1808
1969
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
1809
|
-
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1970
|
+
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1971
|
+
Effect9.map(Option6.some),
|
|
1972
|
+
Effect9.orElseSucceed(() => Option6.none())
|
|
1973
|
+
);
|
|
1810
1974
|
}
|
|
1811
1975
|
}
|
|
1812
1976
|
return Option6.none();
|
|
@@ -1815,45 +1979,44 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
|
1815
1979
|
|
|
1816
1980
|
// src/services/Identity.ts
|
|
1817
1981
|
import { ServiceMap as ServiceMap3 } from "effect";
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
}
|
|
1982
|
+
var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
|
|
1983
|
+
};
|
|
1821
1984
|
|
|
1822
1985
|
// src/services/EpochStore.ts
|
|
1823
1986
|
import { ServiceMap as ServiceMap4 } from "effect";
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1987
|
+
var EpochStore = class extends ServiceMap4.Service()(
|
|
1988
|
+
"tablinum/EpochStore"
|
|
1989
|
+
) {
|
|
1990
|
+
};
|
|
1827
1991
|
|
|
1828
1992
|
// src/services/Storage.ts
|
|
1829
1993
|
import { ServiceMap as ServiceMap5 } from "effect";
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
}
|
|
1994
|
+
var Storage = class extends ServiceMap5.Service()("tablinum/Storage") {
|
|
1995
|
+
};
|
|
1833
1996
|
|
|
1834
1997
|
// src/services/Relay.ts
|
|
1835
1998
|
import { ServiceMap as ServiceMap6 } from "effect";
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
}
|
|
1999
|
+
var Relay = class extends ServiceMap6.Service()("tablinum/Relay") {
|
|
2000
|
+
};
|
|
1839
2001
|
|
|
1840
2002
|
// src/services/GiftWrap.ts
|
|
1841
2003
|
import { ServiceMap as ServiceMap7 } from "effect";
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
}
|
|
2004
|
+
var GiftWrap3 = class extends ServiceMap7.Service()("tablinum/GiftWrap") {
|
|
2005
|
+
};
|
|
1845
2006
|
|
|
1846
2007
|
// src/services/PublishQueue.ts
|
|
1847
2008
|
import { ServiceMap as ServiceMap8 } from "effect";
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2009
|
+
var PublishQueue = class extends ServiceMap8.Service()(
|
|
2010
|
+
"tablinum/PublishQueue"
|
|
2011
|
+
) {
|
|
2012
|
+
};
|
|
1851
2013
|
|
|
1852
2014
|
// src/services/SyncStatus.ts
|
|
1853
2015
|
import { ServiceMap as ServiceMap9 } from "effect";
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2016
|
+
var SyncStatus = class extends ServiceMap9.Service()(
|
|
2017
|
+
"tablinum/SyncStatus"
|
|
2018
|
+
) {
|
|
2019
|
+
};
|
|
1857
2020
|
|
|
1858
2021
|
// src/layers/IdentityLive.ts
|
|
1859
2022
|
import { Effect as Effect11, Layer as Layer2 } from "effect";
|
|
@@ -1894,47 +2057,59 @@ function createIdentity(suppliedKey) {
|
|
|
1894
2057
|
}
|
|
1895
2058
|
|
|
1896
2059
|
// src/layers/IdentityLive.ts
|
|
1897
|
-
var IdentityLive = Layer2.effect(
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
})
|
|
2060
|
+
var IdentityLive = Layer2.effect(
|
|
2061
|
+
Identity,
|
|
2062
|
+
Effect11.gen(function* () {
|
|
2063
|
+
const config = yield* Config;
|
|
2064
|
+
const storage = yield* Storage;
|
|
2065
|
+
const idbKey = yield* storage.getMeta("identity_key");
|
|
2066
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
|
|
2067
|
+
const identity = yield* createIdentity(resolvedKey);
|
|
2068
|
+
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2069
|
+
yield* Effect11.logInfo("Identity loaded", {
|
|
2070
|
+
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2071
|
+
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2072
|
+
});
|
|
2073
|
+
return identity;
|
|
2074
|
+
})
|
|
2075
|
+
);
|
|
1910
2076
|
|
|
1911
2077
|
// src/layers/EpochStoreLive.ts
|
|
1912
2078
|
import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
|
|
1913
2079
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1914
2080
|
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
|
-
}
|
|
2081
|
+
var EpochStoreLive = Layer3.effect(
|
|
2082
|
+
EpochStore,
|
|
2083
|
+
Effect12.gen(function* () {
|
|
2084
|
+
const config = yield* Config;
|
|
2085
|
+
const identity = yield* Identity;
|
|
2086
|
+
const storage = yield* Storage;
|
|
2087
|
+
const idbRaw = yield* storage.getMeta("epochs");
|
|
2088
|
+
if (typeof idbRaw === "string") {
|
|
2089
|
+
const idbStore = deserializeEpochStore(idbRaw);
|
|
2090
|
+
if (Option7.isSome(idbStore)) {
|
|
2091
|
+
yield* Effect12.logInfo("Epoch store loaded", {
|
|
2092
|
+
source: "storage",
|
|
2093
|
+
epochs: idbStore.value.epochs.size
|
|
2094
|
+
});
|
|
2095
|
+
return idbStore.value;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
2099
|
+
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2100
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2101
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2102
|
+
return store2;
|
|
2103
|
+
}
|
|
2104
|
+
const store = createEpochStoreFromInputs(
|
|
2105
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
|
|
2106
|
+
{ createdBy: identity.publicKey }
|
|
2107
|
+
);
|
|
2108
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2109
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2110
|
+
return store;
|
|
2111
|
+
})
|
|
2112
|
+
);
|
|
1938
2113
|
|
|
1939
2114
|
// src/layers/StorageLive.ts
|
|
1940
2115
|
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
@@ -1942,6 +2117,68 @@ import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
|
1942
2117
|
// src/storage/idb.ts
|
|
1943
2118
|
import { Effect as Effect13 } from "effect";
|
|
1944
2119
|
import { openDB } from "idb";
|
|
2120
|
+
|
|
2121
|
+
// src/sync/compact-event.ts
|
|
2122
|
+
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2123
|
+
var VERSION = 1;
|
|
2124
|
+
var HEADER_SIZE = 133;
|
|
2125
|
+
function base64ToBytes(base64) {
|
|
2126
|
+
const binary = atob(base64);
|
|
2127
|
+
const bytes = new Uint8Array(binary.length);
|
|
2128
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2129
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2130
|
+
}
|
|
2131
|
+
return bytes;
|
|
2132
|
+
}
|
|
2133
|
+
function bytesToBase64(bytes) {
|
|
2134
|
+
let binary = "";
|
|
2135
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2136
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2137
|
+
}
|
|
2138
|
+
return btoa(binary);
|
|
2139
|
+
}
|
|
2140
|
+
function packEvent(event) {
|
|
2141
|
+
const pubkey = hexToBytes4(event.pubkey);
|
|
2142
|
+
const sig = hexToBytes4(event.sig);
|
|
2143
|
+
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2144
|
+
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2145
|
+
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2146
|
+
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2147
|
+
}
|
|
2148
|
+
const recipient = hexToBytes4(recipientTag[1]);
|
|
2149
|
+
const createdAtBuf = new Uint8Array(4);
|
|
2150
|
+
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2151
|
+
const content = base64ToBytes(event.content);
|
|
2152
|
+
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2153
|
+
result[0] = VERSION;
|
|
2154
|
+
result.set(pubkey, 1);
|
|
2155
|
+
result.set(sig, 33);
|
|
2156
|
+
result.set(recipient, 97);
|
|
2157
|
+
result.set(createdAtBuf, 129);
|
|
2158
|
+
result.set(content, HEADER_SIZE);
|
|
2159
|
+
return result;
|
|
2160
|
+
}
|
|
2161
|
+
function unpackEvent(id, compact) {
|
|
2162
|
+
const version = compact[0];
|
|
2163
|
+
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2164
|
+
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2165
|
+
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2166
|
+
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2167
|
+
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2168
|
+
const createdAt = dv.getUint32(0, false);
|
|
2169
|
+
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2170
|
+
return {
|
|
2171
|
+
id,
|
|
2172
|
+
pubkey,
|
|
2173
|
+
sig,
|
|
2174
|
+
created_at: createdAt,
|
|
2175
|
+
kind: 1059,
|
|
2176
|
+
tags: [["p", recipient]],
|
|
2177
|
+
content
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/storage/idb.ts
|
|
1945
2182
|
var DB_NAME = "tablinum";
|
|
1946
2183
|
function storeName(collection2) {
|
|
1947
2184
|
return `col_${collection2}`;
|
|
@@ -1972,7 +2209,7 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1972
2209
|
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
1973
2210
|
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
1974
2211
|
}
|
|
1975
|
-
const expectedStores = new Set;
|
|
2212
|
+
const expectedStores = /* @__PURE__ */ new Set();
|
|
1976
2213
|
for (const [, def] of Object.entries(schema)) {
|
|
1977
2214
|
const sn = storeName(def.name);
|
|
1978
2215
|
expectedStores.add(sn);
|
|
@@ -1986,12 +2223,10 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1986
2223
|
const existingIndices = new Set(Array.from(store.indexNames));
|
|
1987
2224
|
const wantedIndices = new Set(def.indices ?? []);
|
|
1988
2225
|
for (const idx of existingIndices) {
|
|
1989
|
-
if (!wantedIndices.has(idx))
|
|
1990
|
-
store.deleteIndex(idx);
|
|
2226
|
+
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
1991
2227
|
}
|
|
1992
2228
|
for (const idx of wantedIndices) {
|
|
1993
|
-
if (!existingIndices.has(idx))
|
|
1994
|
-
store.createIndex(idx, idx);
|
|
2229
|
+
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
1995
2230
|
}
|
|
1996
2231
|
}
|
|
1997
2232
|
}
|
|
@@ -2006,6 +2241,13 @@ function openIDBStorage(dbName, schema) {
|
|
|
2006
2241
|
return Effect13.gen(function* () {
|
|
2007
2242
|
const name = dbName ?? DB_NAME;
|
|
2008
2243
|
const schemaSig = computeSchemaSig(schema);
|
|
2244
|
+
if (typeof indexedDB === "undefined") {
|
|
2245
|
+
return yield* Effect13.fail(
|
|
2246
|
+
new StorageError({
|
|
2247
|
+
message: "IndexedDB is not available in this environment"
|
|
2248
|
+
})
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2009
2251
|
const probeDb = yield* Effect13.tryPromise({
|
|
2010
2252
|
try: () => openDB(name),
|
|
2011
2253
|
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
@@ -2016,7 +2258,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2016
2258
|
const storedSig = yield* Effect13.tryPromise({
|
|
2017
2259
|
try: () => probeDb.get("_meta", "schema_sig"),
|
|
2018
2260
|
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
2019
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(
|
|
2261
|
+
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
2020
2262
|
needsUpgrade = storedSig !== schemaSig;
|
|
2021
2263
|
}
|
|
2022
2264
|
probeDb.close();
|
|
@@ -2033,9 +2275,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2033
2275
|
});
|
|
2034
2276
|
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2035
2277
|
const handle = {
|
|
2036
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() =>
|
|
2037
|
-
return;
|
|
2038
|
-
})),
|
|
2278
|
+
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
2039
2279
|
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2040
2280
|
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2041
2281
|
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
@@ -2055,29 +2295,52 @@ function openIDBStorage(dbName, schema) {
|
|
|
2055
2295
|
}
|
|
2056
2296
|
return results;
|
|
2057
2297
|
}),
|
|
2058
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() =>
|
|
2059
|
-
return;
|
|
2060
|
-
})),
|
|
2298
|
+
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2061
2299
|
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2062
2300
|
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 });
|
|
2301
|
+
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2302
|
+
"getEventsByRecord",
|
|
2303
|
+
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2304
|
+
),
|
|
2305
|
+
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2306
|
+
if (gw.event) {
|
|
2307
|
+
const compact = packEvent(gw.event);
|
|
2308
|
+
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2309
|
+
} else {
|
|
2310
|
+
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2076
2311
|
}
|
|
2077
2312
|
}),
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2313
|
+
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2314
|
+
const raw = await db.get("giftwraps", id);
|
|
2315
|
+
if (!raw) return void 0;
|
|
2316
|
+
if (raw.compact) {
|
|
2317
|
+
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2318
|
+
}
|
|
2319
|
+
if (raw.event) {
|
|
2320
|
+
const compact = packEvent(raw.event);
|
|
2321
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2322
|
+
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2323
|
+
}
|
|
2324
|
+
return { id: raw.id, createdAt: raw.createdAt };
|
|
2325
|
+
}),
|
|
2326
|
+
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2327
|
+
const raws = await db.getAll("giftwraps");
|
|
2328
|
+
const results = [];
|
|
2329
|
+
for (const raw of raws) {
|
|
2330
|
+
if (raw.compact) {
|
|
2331
|
+
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2332
|
+
} else if (raw.event) {
|
|
2333
|
+
const compact = packEvent(raw.event);
|
|
2334
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2335
|
+
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2336
|
+
} else {
|
|
2337
|
+
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
return results;
|
|
2341
|
+
}),
|
|
2342
|
+
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2343
|
+
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2081
2344
|
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2082
2345
|
const existing = await db.get("events", id);
|
|
2083
2346
|
if (existing) {
|
|
@@ -2085,9 +2348,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2085
2348
|
}
|
|
2086
2349
|
}),
|
|
2087
2350
|
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2088
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() =>
|
|
2089
|
-
return;
|
|
2090
|
-
})),
|
|
2351
|
+
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2091
2352
|
close: () => Effect13.sync(() => db.close())
|
|
2092
2353
|
};
|
|
2093
2354
|
return handle;
|
|
@@ -2095,15 +2356,18 @@ function openIDBStorage(dbName, schema) {
|
|
|
2095
2356
|
}
|
|
2096
2357
|
|
|
2097
2358
|
// src/layers/StorageLive.ts
|
|
2098
|
-
var StorageLive = Layer4.effect(
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
})
|
|
2359
|
+
var StorageLive = Layer4.effect(
|
|
2360
|
+
Storage,
|
|
2361
|
+
Effect14.gen(function* () {
|
|
2362
|
+
const config = yield* Config;
|
|
2363
|
+
const handle = yield* openIDBStorage(config.dbName, {
|
|
2364
|
+
...config.schema,
|
|
2365
|
+
_members: membersCollectionDef
|
|
2366
|
+
});
|
|
2367
|
+
yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
|
|
2368
|
+
return handle;
|
|
2369
|
+
})
|
|
2370
|
+
);
|
|
2107
2371
|
|
|
2108
2372
|
// src/layers/RelayLive.ts
|
|
2109
2373
|
import { Layer as Layer5 } from "effect";
|
|
@@ -2117,32 +2381,41 @@ var NegMessageFrameSchema = Schema7.Tuple([
|
|
|
2117
2381
|
Schema7.String
|
|
2118
2382
|
]);
|
|
2119
2383
|
var NegErrorFrameSchema = Schema7.Tuple([Schema7.Literal("NEG-ERR"), Schema7.String, Schema7.String]);
|
|
2120
|
-
var decodeNegFrame = Schema7.decodeUnknownEffect(
|
|
2384
|
+
var decodeNegFrame = Schema7.decodeUnknownEffect(
|
|
2385
|
+
Schema7.fromJsonString(Schema7.Union([NegMessageFrameSchema, NegErrorFrameSchema]))
|
|
2386
|
+
);
|
|
2121
2387
|
function parseNegMessageFrame(data) {
|
|
2122
|
-
return Effect15.runSync(
|
|
2388
|
+
return Effect15.runSync(
|
|
2389
|
+
decodeNegFrame(data).pipe(
|
|
2390
|
+
Effect15.map(Option8.some),
|
|
2391
|
+
Effect15.orElseSucceed(() => Option8.none())
|
|
2392
|
+
)
|
|
2393
|
+
);
|
|
2123
2394
|
}
|
|
2124
2395
|
function createRelayHandle() {
|
|
2125
2396
|
return Effect15.gen(function* () {
|
|
2126
2397
|
const relayScope = yield* Effect15.scope;
|
|
2127
2398
|
const connections = yield* ScopedCache.make({
|
|
2128
2399
|
capacity: 64,
|
|
2129
|
-
lookup: (url) => Effect15.acquireRelease(
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2400
|
+
lookup: (url) => Effect15.acquireRelease(
|
|
2401
|
+
Effect15.tryPromise({
|
|
2402
|
+
try: () => Relay2.connect(url),
|
|
2403
|
+
catch: (e) => new RelayError({
|
|
2404
|
+
message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2405
|
+
url,
|
|
2406
|
+
cause: e
|
|
2407
|
+
})
|
|
2408
|
+
}),
|
|
2409
|
+
(relay) => Effect15.sync(() => {
|
|
2410
|
+
relay.close();
|
|
2135
2411
|
})
|
|
2136
|
-
|
|
2137
|
-
relay.close();
|
|
2138
|
-
}))
|
|
2412
|
+
)
|
|
2139
2413
|
});
|
|
2140
|
-
const connectedUrls = new Set;
|
|
2141
|
-
const statusListeners = new Set;
|
|
2414
|
+
const connectedUrls = /* @__PURE__ */ new Set();
|
|
2415
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
2142
2416
|
const notifyStatus = () => {
|
|
2143
2417
|
const status = { connectedUrls: [...connectedUrls] };
|
|
2144
|
-
for (const listener of statusListeners)
|
|
2145
|
-
listener(status);
|
|
2418
|
+
for (const listener of statusListeners) listener(status);
|
|
2146
2419
|
};
|
|
2147
2420
|
const markConnected = (url) => {
|
|
2148
2421
|
if (!connectedUrls.has(url)) {
|
|
@@ -2156,66 +2429,98 @@ function createRelayHandle() {
|
|
|
2156
2429
|
notifyStatus();
|
|
2157
2430
|
}
|
|
2158
2431
|
};
|
|
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
|
-
|
|
2432
|
+
const getRelay = (url) => ScopedCache.get(connections, url).pipe(
|
|
2433
|
+
Effect15.flatMap(
|
|
2434
|
+
(relay) => relay.connected === false ? ScopedCache.invalidate(connections, url).pipe(
|
|
2435
|
+
Effect15.andThen(ScopedCache.get(connections, url))
|
|
2436
|
+
) : Effect15.succeed(relay)
|
|
2437
|
+
)
|
|
2438
|
+
);
|
|
2439
|
+
const withRelay = (url, run) => getRelay(url).pipe(
|
|
2440
|
+
Effect15.tap(() => Effect15.sync(() => markConnected(url))),
|
|
2441
|
+
Effect15.flatMap((relay) => run(relay)),
|
|
2442
|
+
Effect15.tapError(
|
|
2443
|
+
() => ScopedCache.invalidate(connections, url).pipe(
|
|
2444
|
+
Effect15.tap(() => Effect15.sync(() => markDisconnected(url)))
|
|
2445
|
+
)
|
|
2446
|
+
)
|
|
2447
|
+
);
|
|
2448
|
+
const collectEvents = (url, filters) => withRelay(
|
|
2449
|
+
url,
|
|
2450
|
+
(relay) => Effect15.callback((resume) => {
|
|
2451
|
+
const events = [];
|
|
2452
|
+
let settled = false;
|
|
2453
|
+
let timer;
|
|
2454
|
+
let sub;
|
|
2455
|
+
const cleanup = () => {
|
|
2456
|
+
settled = true;
|
|
2457
|
+
if (timer !== void 0) {
|
|
2458
|
+
clearTimeout(timer);
|
|
2459
|
+
timer = void 0;
|
|
2460
|
+
}
|
|
2461
|
+
sub?.close();
|
|
2462
|
+
sub = void 0;
|
|
2463
|
+
};
|
|
2464
|
+
const fail = (e) => resume(
|
|
2465
|
+
Effect15.fail(
|
|
2466
|
+
new RelayError({
|
|
2467
|
+
message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2468
|
+
url,
|
|
2469
|
+
cause: e
|
|
2470
|
+
})
|
|
2471
|
+
)
|
|
2472
|
+
);
|
|
2473
|
+
try {
|
|
2474
|
+
sub = relay.subscribe([...filters], {
|
|
2475
|
+
onevent(evt) {
|
|
2476
|
+
if (!settled) {
|
|
2477
|
+
events.push(evt);
|
|
2478
|
+
}
|
|
2479
|
+
},
|
|
2480
|
+
oneose() {
|
|
2481
|
+
if (settled) return;
|
|
2482
|
+
cleanup();
|
|
2483
|
+
resume(Effect15.succeed(events));
|
|
2185
2484
|
}
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
if (settled)
|
|
2189
|
-
return;
|
|
2485
|
+
});
|
|
2486
|
+
timer = setTimeout(() => {
|
|
2487
|
+
if (settled) return;
|
|
2190
2488
|
cleanup();
|
|
2191
2489
|
resume(Effect15.succeed(events));
|
|
2192
|
-
}
|
|
2193
|
-
})
|
|
2194
|
-
timer = setTimeout(() => {
|
|
2195
|
-
if (settled)
|
|
2196
|
-
return;
|
|
2490
|
+
}, 1e4);
|
|
2491
|
+
} catch (e) {
|
|
2197
2492
|
cleanup();
|
|
2198
|
-
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
}
|
|
2204
|
-
return Effect15.sync(cleanup);
|
|
2205
|
-
}));
|
|
2493
|
+
fail(e);
|
|
2494
|
+
}
|
|
2495
|
+
return Effect15.sync(cleanup);
|
|
2496
|
+
})
|
|
2497
|
+
);
|
|
2206
2498
|
return {
|
|
2207
2499
|
publish: (event, urls) => Effect15.gen(function* () {
|
|
2208
|
-
const results = yield* Effect15.forEach(
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2500
|
+
const results = yield* Effect15.forEach(
|
|
2501
|
+
urls,
|
|
2502
|
+
(url) => Effect15.result(
|
|
2503
|
+
withRelay(
|
|
2504
|
+
url,
|
|
2505
|
+
(relay) => Effect15.tryPromise({
|
|
2506
|
+
try: () => relay.publish(event),
|
|
2507
|
+
catch: (e) => new RelayError({
|
|
2508
|
+
message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2509
|
+
url,
|
|
2510
|
+
cause: e
|
|
2511
|
+
})
|
|
2512
|
+
}).pipe(
|
|
2513
|
+
Effect15.timeoutOrElse({
|
|
2514
|
+
duration: "10 seconds",
|
|
2515
|
+
onTimeout: () => Effect15.fail(
|
|
2516
|
+
new RelayError({ message: `Publish to ${url} timed out`, url })
|
|
2517
|
+
)
|
|
2518
|
+
})
|
|
2519
|
+
)
|
|
2520
|
+
)
|
|
2521
|
+
),
|
|
2522
|
+
{ concurrency: "unbounded" }
|
|
2523
|
+
);
|
|
2219
2524
|
const failures = results.filter((r) => r._tag === "Failure");
|
|
2220
2525
|
if (failures.length === urls.length && urls.length > 0) {
|
|
2221
2526
|
return yield* new RelayError({
|
|
@@ -2224,90 +2529,104 @@ function createRelayHandle() {
|
|
|
2224
2529
|
}
|
|
2225
2530
|
}),
|
|
2226
2531
|
fetchEvents: (ids, url) => Effect15.gen(function* () {
|
|
2227
|
-
if (ids.length === 0)
|
|
2228
|
-
return [];
|
|
2532
|
+
if (ids.length === 0) return [];
|
|
2229
2533
|
return yield* collectEvents(url, [{ ids }]);
|
|
2230
2534
|
}),
|
|
2231
2535
|
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) {
|
|
2536
|
+
subscribe: (filter, url, onEvent) => withRelay(
|
|
2537
|
+
url,
|
|
2538
|
+
(relay) => Effect15.acquireRelease(
|
|
2539
|
+
Effect15.try({
|
|
2540
|
+
try: () => relay.subscribe([filter], {
|
|
2541
|
+
onevent(evt) {
|
|
2542
|
+
onEvent(evt);
|
|
2543
|
+
},
|
|
2544
|
+
oneose() {
|
|
2545
|
+
}
|
|
2546
|
+
}),
|
|
2547
|
+
catch: (e) => new RelayError({
|
|
2548
|
+
message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2549
|
+
url,
|
|
2550
|
+
cause: e
|
|
2551
|
+
})
|
|
2552
|
+
}),
|
|
2553
|
+
(sub) => Effect15.sync(() => {
|
|
2554
|
+
sub.close();
|
|
2555
|
+
})
|
|
2556
|
+
).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)
|
|
2557
|
+
),
|
|
2558
|
+
sendNegMsg: (url, subId, filter, msgHex) => withRelay(
|
|
2559
|
+
url,
|
|
2560
|
+
(relay) => Effect15.callback((resume) => {
|
|
2561
|
+
let settled = false;
|
|
2562
|
+
let timer;
|
|
2563
|
+
let sub;
|
|
2564
|
+
let ws;
|
|
2565
|
+
const cleanup = () => {
|
|
2566
|
+
settled = true;
|
|
2567
|
+
if (timer !== void 0) {
|
|
2568
|
+
clearTimeout(timer);
|
|
2569
|
+
timer = void 0;
|
|
2570
|
+
}
|
|
2571
|
+
sub?.close();
|
|
2572
|
+
sub = void 0;
|
|
2573
|
+
ws?.removeEventListener("message", handler);
|
|
2574
|
+
ws = void 0;
|
|
2575
|
+
};
|
|
2576
|
+
const fail = (e) => resume(
|
|
2577
|
+
Effect15.fail(
|
|
2578
|
+
new RelayError({
|
|
2579
|
+
message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2580
|
+
url,
|
|
2581
|
+
cause: e
|
|
2582
|
+
})
|
|
2583
|
+
)
|
|
2584
|
+
);
|
|
2585
|
+
const handler = (msg) => {
|
|
2586
|
+
if (settled || typeof msg.data !== "string") return;
|
|
2587
|
+
const frameOpt = parseNegMessageFrame(msg.data);
|
|
2588
|
+
if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId) return;
|
|
2589
|
+
const frame = frameOpt.value;
|
|
2293
2590
|
cleanup();
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2591
|
+
if (frame[0] === "NEG-MSG") {
|
|
2592
|
+
resume(
|
|
2593
|
+
Effect15.succeed({
|
|
2594
|
+
msgHex: frame[2],
|
|
2595
|
+
haveIds: [],
|
|
2596
|
+
needIds: []
|
|
2597
|
+
})
|
|
2598
|
+
);
|
|
2299
2599
|
return;
|
|
2600
|
+
}
|
|
2601
|
+
fail(new Error(`NEG-ERR: ${frame[2]}`));
|
|
2602
|
+
};
|
|
2603
|
+
try {
|
|
2604
|
+
sub = relay.subscribe([filter], {
|
|
2605
|
+
onevent() {
|
|
2606
|
+
},
|
|
2607
|
+
oneose() {
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
ws = relay._ws || relay.ws;
|
|
2611
|
+
if (!ws) {
|
|
2612
|
+
cleanup();
|
|
2613
|
+
fail(new Error("Cannot access relay WebSocket"));
|
|
2614
|
+
return Effect15.succeed(void 0);
|
|
2615
|
+
}
|
|
2616
|
+
timer = setTimeout(() => {
|
|
2617
|
+
if (settled) return;
|
|
2618
|
+
cleanup();
|
|
2619
|
+
fail(new Error("NIP-77 negotiation timeout"));
|
|
2620
|
+
}, 3e4);
|
|
2621
|
+
ws.addEventListener("message", handler);
|
|
2622
|
+
ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
|
|
2623
|
+
} catch (e) {
|
|
2300
2624
|
cleanup();
|
|
2301
|
-
fail(
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
cleanup();
|
|
2307
|
-
fail(e);
|
|
2308
|
-
}
|
|
2309
|
-
return Effect15.sync(cleanup);
|
|
2310
|
-
})),
|
|
2625
|
+
fail(e);
|
|
2626
|
+
}
|
|
2627
|
+
return Effect15.sync(cleanup);
|
|
2628
|
+
})
|
|
2629
|
+
),
|
|
2311
2630
|
closeAll: () => ScopedCache.invalidateAll(connections),
|
|
2312
2631
|
getStatus: () => ({ connectedUrls: [...connectedUrls] }),
|
|
2313
2632
|
subscribeStatus: (callback) => {
|
|
@@ -2359,11 +2678,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
|
2359
2678
|
}
|
|
2360
2679
|
|
|
2361
2680
|
// src/layers/GiftWrapLive.ts
|
|
2362
|
-
var GiftWrapLive = Layer6.effect(
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2681
|
+
var GiftWrapLive = Layer6.effect(
|
|
2682
|
+
GiftWrap3,
|
|
2683
|
+
Effect17.gen(function* () {
|
|
2684
|
+
const identity = yield* Identity;
|
|
2685
|
+
const epochStore = yield* EpochStore;
|
|
2686
|
+
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2687
|
+
})
|
|
2688
|
+
);
|
|
2367
2689
|
|
|
2368
2690
|
// src/layers/PublishQueueLive.ts
|
|
2369
2691
|
import { Effect as Effect19, Layer as Layer7 } from "effect";
|
|
@@ -2377,12 +2699,11 @@ function persist(storage, pending) {
|
|
|
2377
2699
|
function createPublishQueue(storage, relay) {
|
|
2378
2700
|
return Effect18.gen(function* () {
|
|
2379
2701
|
const stored = yield* storage.getMeta(META_KEY);
|
|
2380
|
-
const initial = Array.isArray(stored) ? new Set(stored) : new Set;
|
|
2702
|
+
const initial = Array.isArray(stored) ? new Set(stored) : /* @__PURE__ */ new Set();
|
|
2381
2703
|
const pendingRef = yield* Ref4.make(initial);
|
|
2382
|
-
const listeners = new Set;
|
|
2704
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2383
2705
|
const notify = (pending) => {
|
|
2384
|
-
for (const listener of listeners)
|
|
2385
|
-
listener(pending.size);
|
|
2706
|
+
for (const listener of listeners) listener(pending.size);
|
|
2386
2707
|
};
|
|
2387
2708
|
return {
|
|
2388
2709
|
enqueue: (eventId) => Effect18.gen(function* () {
|
|
@@ -2396,13 +2717,11 @@ function createPublishQueue(storage, relay) {
|
|
|
2396
2717
|
}),
|
|
2397
2718
|
flush: (relayUrls) => Effect18.gen(function* () {
|
|
2398
2719
|
const pending = yield* Ref4.get(pendingRef);
|
|
2399
|
-
if (pending.size === 0)
|
|
2400
|
-
|
|
2401
|
-
const succeeded = new Set;
|
|
2720
|
+
if (pending.size === 0) return;
|
|
2721
|
+
const succeeded = /* @__PURE__ */ new Set();
|
|
2402
2722
|
let consecutiveFailures = 0;
|
|
2403
2723
|
for (const eventId of pending) {
|
|
2404
|
-
if (consecutiveFailures >= 3)
|
|
2405
|
-
break;
|
|
2724
|
+
if (consecutiveFailures >= 3) break;
|
|
2406
2725
|
const gw = yield* storage.getGiftWrap(eventId);
|
|
2407
2726
|
if (!gw || !gw.event) {
|
|
2408
2727
|
succeeded.add(eventId);
|
|
@@ -2412,7 +2731,6 @@ function createPublishQueue(storage, relay) {
|
|
|
2412
2731
|
const result = yield* Effect18.result(relay.publish(gw.event, relayUrls));
|
|
2413
2732
|
if (result._tag === "Success") {
|
|
2414
2733
|
succeeded.add(eventId);
|
|
2415
|
-
yield* storage.stripGiftWrapBlob(eventId);
|
|
2416
2734
|
consecutiveFailures = 0;
|
|
2417
2735
|
} else {
|
|
2418
2736
|
consecutiveFailures++;
|
|
@@ -2440,11 +2758,14 @@ function createPublishQueue(storage, relay) {
|
|
|
2440
2758
|
}
|
|
2441
2759
|
|
|
2442
2760
|
// src/layers/PublishQueueLive.ts
|
|
2443
|
-
var PublishQueueLive = Layer7.effect(
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2761
|
+
var PublishQueueLive = Layer7.effect(
|
|
2762
|
+
PublishQueue,
|
|
2763
|
+
Effect19.gen(function* () {
|
|
2764
|
+
const storage = yield* Storage;
|
|
2765
|
+
const relay = yield* Relay;
|
|
2766
|
+
return yield* createPublishQueue(storage, relay);
|
|
2767
|
+
})
|
|
2768
|
+
);
|
|
2448
2769
|
|
|
2449
2770
|
// src/layers/SyncStatusLive.ts
|
|
2450
2771
|
import { Layer as Layer8 } from "effect";
|
|
@@ -2454,13 +2775,12 @@ import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
|
2454
2775
|
function createSyncStatusHandle() {
|
|
2455
2776
|
return Effect20.gen(function* () {
|
|
2456
2777
|
const ref = yield* SubscriptionRef.make("idle");
|
|
2457
|
-
const listeners = new Set;
|
|
2778
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2458
2779
|
return {
|
|
2459
2780
|
get: () => SubscriptionRef.get(ref),
|
|
2460
2781
|
set: (status) => Effect20.gen(function* () {
|
|
2461
2782
|
yield* SubscriptionRef.set(ref, status);
|
|
2462
|
-
for (const listener of listeners)
|
|
2463
|
-
listener(status);
|
|
2783
|
+
for (const listener of listeners) listener(status);
|
|
2464
2784
|
}),
|
|
2465
2785
|
subscribe: (callback) => {
|
|
2466
2786
|
listeners.add(callback);
|
|
@@ -2475,8 +2795,7 @@ var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
|
|
|
2475
2795
|
|
|
2476
2796
|
// src/layers/TablinumLive.ts
|
|
2477
2797
|
function reportSyncError(onSyncError, error) {
|
|
2478
|
-
if (!onSyncError)
|
|
2479
|
-
return;
|
|
2798
|
+
if (!onSyncError) return;
|
|
2480
2799
|
onSyncError(error instanceof Error ? error : new Error(String(error)));
|
|
2481
2800
|
}
|
|
2482
2801
|
function mapMemberRecord(record) {
|
|
@@ -2484,249 +2803,352 @@ function mapMemberRecord(record) {
|
|
|
2484
2803
|
id: record.id,
|
|
2485
2804
|
addedAt: record.addedAt,
|
|
2486
2805
|
addedInEpoch: record.addedInEpoch,
|
|
2487
|
-
...record.name !==
|
|
2488
|
-
...record.picture !==
|
|
2489
|
-
...record.about !==
|
|
2490
|
-
...record.nip05 !==
|
|
2491
|
-
...record.removedAt !==
|
|
2492
|
-
...record.removedInEpoch !==
|
|
2806
|
+
...record.name !== void 0 ? { name: record.name } : {},
|
|
2807
|
+
...record.picture !== void 0 ? { picture: record.picture } : {},
|
|
2808
|
+
...record.about !== void 0 ? { about: record.about } : {},
|
|
2809
|
+
...record.nip05 !== void 0 ? { nip05: record.nip05 } : {},
|
|
2810
|
+
...record.removedAt !== void 0 ? { removedAt: record.removedAt } : {},
|
|
2811
|
+
...record.removedInEpoch !== void 0 ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2493
2812
|
};
|
|
2494
2813
|
}
|
|
2495
2814
|
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
|
-
|
|
2815
|
+
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2816
|
+
Layer9.provide(IdentityWithDeps),
|
|
2817
|
+
Layer9.provide(StorageLive)
|
|
2818
|
+
);
|
|
2819
|
+
var GiftWrapWithDeps = GiftWrapLive.pipe(
|
|
2820
|
+
Layer9.provide(IdentityWithDeps),
|
|
2821
|
+
Layer9.provide(EpochStoreWithDeps)
|
|
2822
|
+
);
|
|
2823
|
+
var PublishQueueWithDeps = PublishQueueLive.pipe(
|
|
2824
|
+
Layer9.provide(StorageLive),
|
|
2825
|
+
Layer9.provide(RelayLive)
|
|
2826
|
+
);
|
|
2827
|
+
var AllServicesLive = Layer9.mergeAll(
|
|
2828
|
+
IdentityWithDeps,
|
|
2829
|
+
EpochStoreWithDeps,
|
|
2830
|
+
StorageLive,
|
|
2831
|
+
RelayLive,
|
|
2832
|
+
GiftWrapWithDeps,
|
|
2833
|
+
PublishQueueWithDeps,
|
|
2834
|
+
SyncStatusLive
|
|
2835
|
+
);
|
|
2836
|
+
var TablinumLive = Layer9.effect(
|
|
2837
|
+
Tablinum,
|
|
2838
|
+
Effect21.gen(function* () {
|
|
2839
|
+
const config = yield* Config;
|
|
2840
|
+
const identity = yield* Identity;
|
|
2841
|
+
const epochStore = yield* EpochStore;
|
|
2842
|
+
const storage = yield* Storage;
|
|
2843
|
+
const relay = yield* Relay;
|
|
2844
|
+
const giftWrap = yield* GiftWrap3;
|
|
2845
|
+
const publishQueue = yield* PublishQueue;
|
|
2846
|
+
const syncStatus = yield* SyncStatus;
|
|
2847
|
+
const scope = yield* Effect21.scope;
|
|
2848
|
+
const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
|
|
2849
|
+
const pubsub = yield* PubSub2.unbounded();
|
|
2850
|
+
const replayingRef = yield* Ref5.make(false);
|
|
2851
|
+
const closedRef = yield* Ref5.make(false);
|
|
2852
|
+
const watchCtx = { pubsub, replayingRef };
|
|
2853
|
+
const schemaEntries = Object.entries(config.schema);
|
|
2854
|
+
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2855
|
+
const knownCollections = new Map(
|
|
2856
|
+
allSchemaEntries.map(([, def]) => [def.name, def.eventRetention])
|
|
2857
|
+
);
|
|
2858
|
+
let notifyAuthor;
|
|
2859
|
+
const syncHandle = createSyncHandle(
|
|
2860
|
+
storage,
|
|
2861
|
+
giftWrap,
|
|
2862
|
+
relay,
|
|
2863
|
+
publishQueue,
|
|
2864
|
+
syncStatus,
|
|
2865
|
+
watchCtx,
|
|
2866
|
+
config.relays,
|
|
2867
|
+
knownCollections,
|
|
2868
|
+
epochStore,
|
|
2869
|
+
identity.privateKey,
|
|
2870
|
+
identity.publicKey,
|
|
2871
|
+
scope,
|
|
2872
|
+
config.logLevel,
|
|
2873
|
+
config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : void 0,
|
|
2874
|
+
(pubkey) => notifyAuthor?.(pubkey),
|
|
2875
|
+
config.onRemoved,
|
|
2876
|
+
config.onMembersChanged
|
|
2877
|
+
);
|
|
2878
|
+
const onWrite = (event) => Effect21.gen(function* () {
|
|
2879
|
+
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2880
|
+
const dTag = `${event.collection}:${event.recordId}`;
|
|
2881
|
+
const wrapResult = yield* Effect21.result(
|
|
2882
|
+
giftWrap.wrap({
|
|
2883
|
+
kind: 1,
|
|
2884
|
+
content,
|
|
2885
|
+
tags: [["d", dTag]],
|
|
2886
|
+
created_at: Math.floor(event.createdAt / 1e3)
|
|
2887
|
+
})
|
|
2888
|
+
);
|
|
2889
|
+
if (wrapResult._tag === "Failure") {
|
|
2890
|
+
reportSyncError(config.onSyncError, wrapResult.failure);
|
|
2891
|
+
return;
|
|
2543
2892
|
}
|
|
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"
|
|
2893
|
+
const gw = wrapResult.success;
|
|
2894
|
+
yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
2895
|
+
yield* Effect21.forkIn(
|
|
2896
|
+
Effect21.gen(function* () {
|
|
2897
|
+
const publishResult = yield* Effect21.result(
|
|
2898
|
+
syncHandle.publishLocal({
|
|
2899
|
+
id: gw.id,
|
|
2900
|
+
event: gw,
|
|
2901
|
+
createdAt: gw.created_at
|
|
2902
|
+
})
|
|
2903
|
+
);
|
|
2904
|
+
if (publishResult._tag === "Failure") {
|
|
2905
|
+
reportSyncError(config.onSyncError, publishResult.failure);
|
|
2906
|
+
}
|
|
2907
|
+
}),
|
|
2908
|
+
scope
|
|
2909
|
+
);
|
|
2565
2910
|
});
|
|
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
|
|
2911
|
+
const knownAuthors = /* @__PURE__ */ new Set();
|
|
2912
|
+
const putMemberRecord = (record) => Effect21.gen(function* () {
|
|
2913
|
+
const existing = yield* storage.getRecord("_members", record.id);
|
|
2914
|
+
const event = {
|
|
2915
|
+
id: uuidv7(),
|
|
2916
|
+
collection: "_members",
|
|
2917
|
+
recordId: record.id,
|
|
2918
|
+
kind: existing ? "u" : "c",
|
|
2919
|
+
data: record,
|
|
2920
|
+
createdAt: Date.now(),
|
|
2921
|
+
author: identity.publicKey
|
|
2922
|
+
};
|
|
2923
|
+
yield* storage.putEvent(event);
|
|
2924
|
+
yield* applyEvent(storage, event);
|
|
2925
|
+
yield* onWrite(event);
|
|
2926
|
+
yield* notifyChange(watchCtx, {
|
|
2927
|
+
collection: "_members",
|
|
2928
|
+
recordId: record.id,
|
|
2929
|
+
kind: existing ? "update" : "create"
|
|
2930
|
+
});
|
|
2931
|
+
config.onMembersChanged?.();
|
|
2618
2932
|
});
|
|
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
|
-
|
|
2933
|
+
notifyAuthor = (pubkey) => {
|
|
2934
|
+
if (knownAuthors.has(pubkey)) return;
|
|
2935
|
+
knownAuthors.add(pubkey);
|
|
2936
|
+
Effect21.runFork(
|
|
2937
|
+
Effect21.gen(function* () {
|
|
2938
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
2939
|
+
if (!existing) {
|
|
2940
|
+
yield* putMemberRecord({
|
|
2941
|
+
id: pubkey,
|
|
2942
|
+
addedAt: Date.now(),
|
|
2943
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(
|
|
2947
|
+
Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none()))
|
|
2948
|
+
);
|
|
2949
|
+
if (Option9.isSome(profileOpt)) {
|
|
2950
|
+
const current = yield* storage.getRecord("_members", pubkey);
|
|
2951
|
+
if (current) {
|
|
2952
|
+
yield* storage.putRecord("_members", {
|
|
2953
|
+
...current,
|
|
2954
|
+
...profileOpt.value
|
|
2955
|
+
});
|
|
2956
|
+
yield* notifyChange(watchCtx, {
|
|
2957
|
+
collection: "_members",
|
|
2958
|
+
recordId: pubkey,
|
|
2959
|
+
kind: "update"
|
|
2960
|
+
});
|
|
2961
|
+
config.onMembersChanged?.();
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope))
|
|
2965
|
+
);
|
|
2966
|
+
};
|
|
2967
|
+
const handles = /* @__PURE__ */ new Map();
|
|
2968
|
+
for (const [, def] of allSchemaEntries) {
|
|
2969
|
+
const validator = buildValidator(def.name, def);
|
|
2970
|
+
const partialValidator = buildPartialValidator(def.name, def);
|
|
2971
|
+
const handle = createCollectionHandle(
|
|
2972
|
+
def,
|
|
2973
|
+
storage,
|
|
2974
|
+
watchCtx,
|
|
2975
|
+
validator,
|
|
2976
|
+
partialValidator,
|
|
2977
|
+
uuidv7,
|
|
2978
|
+
identity.publicKey,
|
|
2979
|
+
onWrite,
|
|
2980
|
+
config.logLevel
|
|
2981
|
+
);
|
|
2982
|
+
handles.set(def.name, handle);
|
|
2983
|
+
}
|
|
2984
|
+
yield* syncHandle.startSubscription();
|
|
2985
|
+
yield* Effect21.logInfo("Tablinum ready", {
|
|
2986
|
+
dbName: config.dbName,
|
|
2987
|
+
collections: schemaEntries.map(([name]) => name),
|
|
2988
|
+
relays: config.relays
|
|
2989
|
+
});
|
|
2990
|
+
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2991
|
+
if (!selfMember) {
|
|
2666
2992
|
yield* putMemberRecord({
|
|
2667
|
-
id:
|
|
2993
|
+
id: identity.publicKey,
|
|
2668
2994
|
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
|
|
2995
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2690
2996
|
});
|
|
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
|
-
|
|
2997
|
+
}
|
|
2998
|
+
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
2999
|
+
const ensureOpen = (effect) => withLog(
|
|
3000
|
+
Effect21.gen(function* () {
|
|
3001
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3002
|
+
return yield* new StorageError({ message: "Database is closed" });
|
|
3003
|
+
}
|
|
3004
|
+
return yield* effect;
|
|
3005
|
+
})
|
|
3006
|
+
);
|
|
3007
|
+
const ensureSyncOpen = (effect) => withLog(
|
|
3008
|
+
Effect21.gen(function* () {
|
|
3009
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3010
|
+
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
3011
|
+
}
|
|
3012
|
+
return yield* effect;
|
|
3013
|
+
})
|
|
3014
|
+
);
|
|
3015
|
+
const dbHandle = {
|
|
3016
|
+
collection: (name) => {
|
|
3017
|
+
const handle = handles.get(name);
|
|
3018
|
+
if (!handle) throw new Error(`Collection "${name}" not found in schema`);
|
|
3019
|
+
return handle;
|
|
3020
|
+
},
|
|
3021
|
+
publicKey: identity.publicKey,
|
|
3022
|
+
members: handles.get("_members"),
|
|
3023
|
+
exportKey: () => identity.exportKey(),
|
|
3024
|
+
exportInvite: () => ({
|
|
3025
|
+
epochKeys: [...exportEpochKeys(epochStore)],
|
|
3026
|
+
relays: [...config.relays],
|
|
3027
|
+
dbName: config.dbName
|
|
3028
|
+
}),
|
|
3029
|
+
close: () => withLog(
|
|
3030
|
+
Effect21.gen(function* () {
|
|
3031
|
+
if (yield* Ref5.get(closedRef)) return;
|
|
3032
|
+
yield* Ref5.set(closedRef, true);
|
|
3033
|
+
syncHandle.stopHealing();
|
|
3034
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3035
|
+
})
|
|
3036
|
+
),
|
|
3037
|
+
rebuild: () => ensureOpen(
|
|
3038
|
+
rebuild(
|
|
3039
|
+
storage,
|
|
3040
|
+
allSchemaEntries.map(([, def]) => def.name)
|
|
3041
|
+
)
|
|
3042
|
+
),
|
|
3043
|
+
sync: () => ensureSyncOpen(
|
|
3044
|
+
syncHandle.sync().pipe(
|
|
3045
|
+
Effect21.tap(
|
|
3046
|
+
() => Effect21.sync(() => {
|
|
3047
|
+
syncHandle.startHealing();
|
|
3048
|
+
})
|
|
3049
|
+
)
|
|
3050
|
+
)
|
|
3051
|
+
),
|
|
3052
|
+
getSyncStatus: () => syncStatus.get(),
|
|
3053
|
+
subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
|
|
3054
|
+
pendingCount: () => publishQueue.size(),
|
|
3055
|
+
subscribePendingCount: (callback) => publishQueue.subscribe(callback),
|
|
3056
|
+
getRelayStatus: () => relay.getStatus(),
|
|
3057
|
+
subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
|
|
3058
|
+
addMember: (pubkey) => ensureOpen(
|
|
3059
|
+
Effect21.gen(function* () {
|
|
3060
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
3061
|
+
if (existing && !existing.removedAt) return;
|
|
3062
|
+
yield* putMemberRecord({
|
|
3063
|
+
id: pubkey,
|
|
3064
|
+
addedAt: Date.now(),
|
|
3065
|
+
addedInEpoch: getCurrentEpoch(epochStore).id,
|
|
3066
|
+
...existing ? { removedAt: void 0, removedInEpoch: void 0 } : {}
|
|
3067
|
+
});
|
|
3068
|
+
})
|
|
3069
|
+
),
|
|
3070
|
+
removeMember: (pubkey) => ensureOpen(
|
|
3071
|
+
Effect21.gen(function* () {
|
|
3072
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
3073
|
+
const activeMembers = allMembers.filter(
|
|
3074
|
+
(member) => !member.removedAt && member.id !== pubkey
|
|
3075
|
+
);
|
|
3076
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
3077
|
+
const result = createRotation(
|
|
3078
|
+
epochStore,
|
|
3079
|
+
identity.privateKey,
|
|
3080
|
+
identity.publicKey,
|
|
3081
|
+
activePubkeys,
|
|
3082
|
+
[pubkey]
|
|
3083
|
+
);
|
|
3084
|
+
addEpoch(epochStore, result.epoch);
|
|
3085
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
3086
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
3087
|
+
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
3088
|
+
yield* putMemberRecord({
|
|
3089
|
+
...memberRecord ?? {
|
|
3090
|
+
id: pubkey,
|
|
3091
|
+
addedAt: 0,
|
|
3092
|
+
addedInEpoch: EpochId("epoch-0")
|
|
3093
|
+
},
|
|
3094
|
+
removedAt: Date.now(),
|
|
3095
|
+
removedInEpoch: result.epoch.id
|
|
3096
|
+
});
|
|
3097
|
+
yield* Effect21.forEach(
|
|
3098
|
+
result.wrappedEvents,
|
|
3099
|
+
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
3100
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3101
|
+
Effect21.ignore
|
|
3102
|
+
),
|
|
3103
|
+
{ discard: true }
|
|
3104
|
+
);
|
|
3105
|
+
yield* Effect21.forEach(
|
|
3106
|
+
result.removalNotices,
|
|
3107
|
+
(notice) => relay.publish(notice, [...config.relays]).pipe(
|
|
3108
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3109
|
+
Effect21.ignore
|
|
3110
|
+
),
|
|
3111
|
+
{ discard: true }
|
|
3112
|
+
);
|
|
3113
|
+
yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
|
|
3114
|
+
})
|
|
3115
|
+
),
|
|
3116
|
+
getMembers: () => ensureOpen(
|
|
3117
|
+
Effect21.gen(function* () {
|
|
3118
|
+
const allRecords = yield* storage.getAllRecords("_members");
|
|
3119
|
+
return allRecords.filter((record) => !record._d).map(mapMemberRecord);
|
|
3120
|
+
})
|
|
3121
|
+
),
|
|
3122
|
+
getProfile: () => ensureOpen(
|
|
3123
|
+
Effect21.gen(function* () {
|
|
3124
|
+
const record = yield* storage.getRecord("_members", identity.publicKey);
|
|
3125
|
+
if (!record) return {};
|
|
3126
|
+
const profile = {};
|
|
3127
|
+
if (record.name !== void 0) profile.name = record.name;
|
|
3128
|
+
if (record.picture !== void 0) profile.picture = record.picture;
|
|
3129
|
+
if (record.about !== void 0) profile.about = record.about;
|
|
3130
|
+
if (record.nip05 !== void 0) profile.nip05 = record.nip05;
|
|
3131
|
+
return profile;
|
|
3132
|
+
})
|
|
3133
|
+
),
|
|
3134
|
+
setProfile: (profile) => ensureOpen(
|
|
3135
|
+
Effect21.gen(function* () {
|
|
3136
|
+
const existing = yield* storage.getRecord("_members", identity.publicKey);
|
|
3137
|
+
if (!existing) {
|
|
3138
|
+
return yield* new ValidationError({ message: "Current user is not a member" });
|
|
3139
|
+
}
|
|
3140
|
+
const { _d, _u, _a, _e, ...memberFields } = existing;
|
|
3141
|
+
yield* putMemberRecord({ ...memberFields, ...profile });
|
|
3142
|
+
})
|
|
3143
|
+
)
|
|
3144
|
+
};
|
|
3145
|
+
return dbHandle;
|
|
3146
|
+
}).pipe(Effect21.withLogSpan("tablinum.init"))
|
|
3147
|
+
).pipe(Layer9.provide(AllServicesLive));
|
|
2725
3148
|
|
|
2726
3149
|
// src/db/create-tablinum.ts
|
|
2727
3150
|
function resolveLogLevel(input) {
|
|
2728
|
-
if (input ===
|
|
2729
|
-
return "None";
|
|
3151
|
+
if (input === void 0 || input === "none") return "None";
|
|
2730
3152
|
switch (input) {
|
|
2731
3153
|
case "debug":
|
|
2732
3154
|
return "Debug";
|
|
@@ -2786,14 +3208,12 @@ function createDeferred() {
|
|
|
2786
3208
|
promise,
|
|
2787
3209
|
settled: () => settled,
|
|
2788
3210
|
resolve: (value) => {
|
|
2789
|
-
if (settled)
|
|
2790
|
-
return;
|
|
3211
|
+
if (settled) return;
|
|
2791
3212
|
settled = true;
|
|
2792
3213
|
resolvePromise(value);
|
|
2793
3214
|
},
|
|
2794
3215
|
reject: (reason) => {
|
|
2795
|
-
if (settled)
|
|
2796
|
-
return;
|
|
3216
|
+
if (settled) return;
|
|
2797
3217
|
settled = true;
|
|
2798
3218
|
rejectPromise(reason);
|
|
2799
3219
|
}
|
|
@@ -2815,7 +3235,9 @@ function wrapQueryBuilder(getBuilder, touchVersion, ready) {
|
|
|
2815
3235
|
},
|
|
2816
3236
|
first: () => {
|
|
2817
3237
|
touchVersion();
|
|
2818
|
-
return ready.then(
|
|
3238
|
+
return ready.then(
|
|
3239
|
+
() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull))
|
|
3240
|
+
);
|
|
2819
3241
|
},
|
|
2820
3242
|
count: () => {
|
|
2821
3243
|
touchVersion();
|
|
@@ -2847,7 +3269,9 @@ function wrapOrderByBuilder(getBuilder, touchVersion, ready) {
|
|
|
2847
3269
|
},
|
|
2848
3270
|
first: () => {
|
|
2849
3271
|
touchVersion();
|
|
2850
|
-
return ready.then(
|
|
3272
|
+
return ready.then(
|
|
3273
|
+
() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull))
|
|
3274
|
+
);
|
|
2851
3275
|
},
|
|
2852
3276
|
count: () => {
|
|
2853
3277
|
touchVersion();
|
|
@@ -2857,7 +3281,7 @@ function wrapOrderByBuilder(getBuilder, touchVersion, ready) {
|
|
|
2857
3281
|
}
|
|
2858
3282
|
|
|
2859
3283
|
// src/svelte/collection.svelte.ts
|
|
2860
|
-
|
|
3284
|
+
var Collection = class {
|
|
2861
3285
|
error = $state(null);
|
|
2862
3286
|
#handle = null;
|
|
2863
3287
|
#ready = createDeferred();
|
|
@@ -2865,15 +3289,16 @@ class Collection {
|
|
|
2865
3289
|
#watchAbort = null;
|
|
2866
3290
|
#watchFiber = null;
|
|
2867
3291
|
#logLevel = "None";
|
|
3292
|
+
/** @internal */
|
|
2868
3293
|
_bind(handle, logLevel = "None") {
|
|
2869
|
-
if (this.#handle)
|
|
2870
|
-
return;
|
|
3294
|
+
if (this.#handle) return;
|
|
2871
3295
|
this.#handle = handle;
|
|
2872
3296
|
this.#logLevel = logLevel;
|
|
2873
3297
|
this.error = null;
|
|
2874
3298
|
this.#settleReady();
|
|
2875
3299
|
this.#startWatch();
|
|
2876
3300
|
}
|
|
3301
|
+
/** @internal */
|
|
2877
3302
|
_fail(err) {
|
|
2878
3303
|
this.error = err;
|
|
2879
3304
|
this.#settleReady(err);
|
|
@@ -2886,31 +3311,41 @@ class Collection {
|
|
|
2886
3311
|
}
|
|
2887
3312
|
}
|
|
2888
3313
|
#startWatch() {
|
|
2889
|
-
if (!this.#handle)
|
|
2890
|
-
|
|
2891
|
-
const abort = new AbortController;
|
|
3314
|
+
if (!this.#handle) return;
|
|
3315
|
+
const abort = new AbortController();
|
|
2892
3316
|
this.#watchAbort = abort;
|
|
2893
|
-
this.#watchFiber = Effect24.runFork(
|
|
2894
|
-
|
|
2895
|
-
this.#
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3317
|
+
this.#watchFiber = Effect24.runFork(
|
|
3318
|
+
Stream4.runForEach(
|
|
3319
|
+
this.#handle.watch(),
|
|
3320
|
+
(_records) => Effect24.sync(() => {
|
|
3321
|
+
if (!abort.signal.aborted) {
|
|
3322
|
+
this.#version++;
|
|
3323
|
+
}
|
|
3324
|
+
})
|
|
3325
|
+
).pipe(
|
|
3326
|
+
Effect24.catch(
|
|
3327
|
+
(e) => Effect24.sync(() => {
|
|
3328
|
+
if (!abort.signal.aborted) {
|
|
3329
|
+
this.error = e instanceof Error ? e : new Error(String(e));
|
|
3330
|
+
}
|
|
3331
|
+
})
|
|
3332
|
+
),
|
|
3333
|
+
Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)
|
|
3334
|
+
)
|
|
3335
|
+
);
|
|
2902
3336
|
}
|
|
2903
3337
|
#touchVersion = () => {
|
|
2904
|
-
this.#version;
|
|
3338
|
+
void this.#version;
|
|
2905
3339
|
};
|
|
2906
3340
|
#handleOrThrow = () => {
|
|
2907
|
-
if (this.#handle)
|
|
2908
|
-
return this.#handle;
|
|
3341
|
+
if (this.#handle) return this.#handle;
|
|
2909
3342
|
throw this.error ?? new ClosedError({ message: "Collection is not ready" });
|
|
2910
3343
|
};
|
|
2911
3344
|
#run = async (getEffect) => {
|
|
2912
3345
|
await this.#ready.promise;
|
|
2913
|
-
return Effect24.runPromise(
|
|
3346
|
+
return Effect24.runPromise(
|
|
3347
|
+
getEffect().pipe(Effect24.provideService(References5.MinimumLogLevel, this.#logLevel))
|
|
3348
|
+
);
|
|
2914
3349
|
};
|
|
2915
3350
|
add = (data) => {
|
|
2916
3351
|
return this.#run(() => this.#handleOrThrow().add(data));
|
|
@@ -2929,7 +3364,11 @@ class Collection {
|
|
|
2929
3364
|
return this.#run(() => this.#handleOrThrow().get(id));
|
|
2930
3365
|
}
|
|
2931
3366
|
this.#touchVersion();
|
|
2932
|
-
return this.#run(
|
|
3367
|
+
return this.#run(
|
|
3368
|
+
() => Stream4.runHead(this.#handleOrThrow().watch()).pipe(
|
|
3369
|
+
Effect24.map((opt) => Option11.getOrElse(opt, () => []))
|
|
3370
|
+
)
|
|
3371
|
+
);
|
|
2933
3372
|
}
|
|
2934
3373
|
first = () => {
|
|
2935
3374
|
this.#touchVersion();
|
|
@@ -2940,11 +3379,20 @@ class Collection {
|
|
|
2940
3379
|
return this.#run(() => this.#handleOrThrow().count());
|
|
2941
3380
|
};
|
|
2942
3381
|
where = (field2) => {
|
|
2943
|
-
return wrapWhereClause(
|
|
3382
|
+
return wrapWhereClause(
|
|
3383
|
+
() => this.#handleOrThrow().where(field2),
|
|
3384
|
+
this.#touchVersion,
|
|
3385
|
+
this.#ready.promise
|
|
3386
|
+
);
|
|
2944
3387
|
};
|
|
2945
3388
|
orderBy = (field2) => {
|
|
2946
|
-
return wrapOrderByBuilder(
|
|
3389
|
+
return wrapOrderByBuilder(
|
|
3390
|
+
() => this.#handleOrThrow().orderBy(field2),
|
|
3391
|
+
this.#touchVersion,
|
|
3392
|
+
this.#ready.promise
|
|
3393
|
+
);
|
|
2947
3394
|
};
|
|
3395
|
+
/** @internal */
|
|
2948
3396
|
_destroy(reason = new ClosedError({ message: "Collection is closed" })) {
|
|
2949
3397
|
if (this.#watchAbort) {
|
|
2950
3398
|
this.#watchAbort.abort();
|
|
@@ -2958,10 +3406,10 @@ class Collection {
|
|
|
2958
3406
|
this.error ??= reason;
|
|
2959
3407
|
this.#settleReady(this.error);
|
|
2960
3408
|
}
|
|
2961
|
-
}
|
|
3409
|
+
};
|
|
2962
3410
|
|
|
2963
3411
|
// src/svelte/tablinum.svelte.ts
|
|
2964
|
-
|
|
3412
|
+
var Tablinum2 = class {
|
|
2965
3413
|
status = $state("initializing");
|
|
2966
3414
|
syncStatus = $state("idle");
|
|
2967
3415
|
pendingCount = $state(0);
|
|
@@ -2970,8 +3418,8 @@ class Tablinum2 {
|
|
|
2970
3418
|
ready;
|
|
2971
3419
|
#handle = null;
|
|
2972
3420
|
#scope = null;
|
|
2973
|
-
#collections = new Map;
|
|
2974
|
-
#members = new Collection;
|
|
3421
|
+
#collections = /* @__PURE__ */ new Map();
|
|
3422
|
+
#members = new Collection();
|
|
2975
3423
|
#unsubscribeSyncStatus = null;
|
|
2976
3424
|
#unsubscribePendingCount = null;
|
|
2977
3425
|
#unsubscribeRelayStatus = null;
|
|
@@ -3000,16 +3448,20 @@ class Tablinum2 {
|
|
|
3000
3448
|
const handle = this.#requireReady();
|
|
3001
3449
|
try {
|
|
3002
3450
|
this.error = null;
|
|
3003
|
-
return await Effect25.runPromise(
|
|
3451
|
+
return await Effect25.runPromise(
|
|
3452
|
+
run(handle).pipe(Effect25.provideService(References6.MinimumLogLevel, this.#logLevel))
|
|
3453
|
+
);
|
|
3004
3454
|
} catch (e) {
|
|
3005
3455
|
this.error = e instanceof Error ? e : new Error(String(e));
|
|
3006
3456
|
throw this.error;
|
|
3007
3457
|
}
|
|
3008
3458
|
};
|
|
3009
|
-
async#init(config) {
|
|
3459
|
+
async #init(config) {
|
|
3010
3460
|
const scope = Effect25.runSync(Scope6.make());
|
|
3011
3461
|
try {
|
|
3012
|
-
const handle = await Effect25.runPromise(
|
|
3462
|
+
const handle = await Effect25.runPromise(
|
|
3463
|
+
createTablinum(config).pipe(Effect25.provideService(Scope6.Scope, scope))
|
|
3464
|
+
);
|
|
3013
3465
|
if (this.#closed) {
|
|
3014
3466
|
await Effect25.runPromise(Scope6.close(scope, Exit2.void));
|
|
3015
3467
|
this.#scope = null;
|
|
@@ -3032,7 +3484,8 @@ class Tablinum2 {
|
|
|
3032
3484
|
this.status = "ready";
|
|
3033
3485
|
this.#settleReady();
|
|
3034
3486
|
} catch (e) {
|
|
3035
|
-
await Effect25.runPromise(Scope6.close(scope, Exit2.fail(e))).catch(() => {
|
|
3487
|
+
await Effect25.runPromise(Scope6.close(scope, Exit2.fail(e))).catch(() => {
|
|
3488
|
+
});
|
|
3036
3489
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
3037
3490
|
this.error = err;
|
|
3038
3491
|
this.status = "error";
|
|
@@ -3055,7 +3508,7 @@ class Tablinum2 {
|
|
|
3055
3508
|
}
|
|
3056
3509
|
let col = this.#collections.get(name);
|
|
3057
3510
|
if (!col) {
|
|
3058
|
-
col = new Collection;
|
|
3511
|
+
col = new Collection();
|
|
3059
3512
|
this.#collections.set(name, col);
|
|
3060
3513
|
if (this.#handle) {
|
|
3061
3514
|
col._bind(this.#handle.collection(name), this.#logLevel);
|
|
@@ -3076,8 +3529,7 @@ class Tablinum2 {
|
|
|
3076
3529
|
return this.#requireReady().exportInvite();
|
|
3077
3530
|
}
|
|
3078
3531
|
close = async () => {
|
|
3079
|
-
if (this.#closed)
|
|
3080
|
-
return;
|
|
3532
|
+
if (this.#closed) return;
|
|
3081
3533
|
this.#closed = true;
|
|
3082
3534
|
const closeError = new ClosedError({
|
|
3083
3535
|
message: this.#readyState.settled() ? "Tablinum is closed" : "Tablinum was closed before initialization completed"
|
|
@@ -3095,8 +3547,7 @@ class Tablinum2 {
|
|
|
3095
3547
|
this.#unsubscribeRelayStatus = null;
|
|
3096
3548
|
}
|
|
3097
3549
|
this.#members._destroy(closeError);
|
|
3098
|
-
for (const col of this.#collections.values())
|
|
3099
|
-
col._destroy(closeError);
|
|
3550
|
+
for (const col of this.#collections.values()) col._destroy(closeError);
|
|
3100
3551
|
this.#collections.clear();
|
|
3101
3552
|
const handle = this.#handle;
|
|
3102
3553
|
const scope = this.#scope;
|
|
@@ -3119,19 +3570,19 @@ class Tablinum2 {
|
|
|
3119
3570
|
getMembers = async () => this.#runHandleEffect((handle) => handle.getMembers());
|
|
3120
3571
|
getProfile = async () => this.#runHandleEffect((handle) => handle.getProfile());
|
|
3121
3572
|
setProfile = async (profile) => this.#runHandleEffect((handle) => handle.setProfile(profile));
|
|
3122
|
-
}
|
|
3573
|
+
};
|
|
3123
3574
|
export {
|
|
3124
|
-
|
|
3125
|
-
encodeInvite,
|
|
3126
|
-
decodeInvite,
|
|
3127
|
-
collection,
|
|
3128
|
-
ValidationError,
|
|
3129
|
-
Tablinum2 as Tablinum,
|
|
3130
|
-
SyncError,
|
|
3131
|
-
StorageError,
|
|
3132
|
-
RelayError,
|
|
3133
|
-
NotFoundError,
|
|
3134
|
-
CryptoError,
|
|
3575
|
+
ClosedError,
|
|
3135
3576
|
Collection,
|
|
3136
|
-
|
|
3577
|
+
CryptoError,
|
|
3578
|
+
NotFoundError,
|
|
3579
|
+
RelayError,
|
|
3580
|
+
StorageError,
|
|
3581
|
+
SyncError,
|
|
3582
|
+
Tablinum2 as Tablinum,
|
|
3583
|
+
ValidationError,
|
|
3584
|
+
collection,
|
|
3585
|
+
decodeInvite,
|
|
3586
|
+
encodeInvite,
|
|
3587
|
+
field
|
|
3137
3588
|
};
|