tablinum 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1392 -937
- package/dist/storage/idb.d.ts +0 -1
- package/dist/svelte/index.svelte.js +1461 -975
- package/dist/sync/compact-event.d.ts +3 -0
- package/dist/sync/gift-wrap.d.ts +1 -2
- package/dist/sync/sync-service.d.ts +2 -0
- package/package.json +8 -27
- package/README.md +0 -298
package/dist/index.js
CHANGED
|
@@ -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/create-tablinum.ts
|
|
57
60
|
import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
|
|
58
61
|
|
|
@@ -61,27 +64,20 @@ import { Effect, Schema as Schema2 } from "effect";
|
|
|
61
64
|
|
|
62
65
|
// src/errors.ts
|
|
63
66
|
import { Data } from "effect";
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
class NotFoundError extends Data.TaggedError("NotFoundError") {
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
class ClosedError extends Data.TaggedError("ClosedError") {
|
|
84
|
-
}
|
|
67
|
+
var ValidationError = class extends Data.TaggedError("ValidationError") {
|
|
68
|
+
};
|
|
69
|
+
var StorageError = class extends Data.TaggedError("StorageError") {
|
|
70
|
+
};
|
|
71
|
+
var CryptoError = class extends Data.TaggedError("CryptoError") {
|
|
72
|
+
};
|
|
73
|
+
var RelayError = class extends Data.TaggedError("RelayError") {
|
|
74
|
+
};
|
|
75
|
+
var SyncError = class extends Data.TaggedError("SyncError") {
|
|
76
|
+
};
|
|
77
|
+
var NotFoundError = class extends Data.TaggedError("NotFoundError") {
|
|
78
|
+
};
|
|
79
|
+
var ClosedError = class extends Data.TaggedError("ClosedError") {
|
|
80
|
+
};
|
|
85
81
|
|
|
86
82
|
// src/db/epoch.ts
|
|
87
83
|
import { Option, Schema } from "effect";
|
|
@@ -109,15 +105,17 @@ var PersistedEpochStoreSchema = Schema.Struct({
|
|
|
109
105
|
epochs: Schema.Array(PersistedEpochSchema),
|
|
110
106
|
currentEpochId: Schema.String
|
|
111
107
|
});
|
|
112
|
-
var decodePersistedEpochStore = Schema.decodeUnknownSync(
|
|
108
|
+
var decodePersistedEpochStore = Schema.decodeUnknownSync(
|
|
109
|
+
Schema.fromJsonString(PersistedEpochStoreSchema)
|
|
110
|
+
);
|
|
113
111
|
function createEpochKey(id, privateKeyHex, createdBy, parentEpoch) {
|
|
114
112
|
const publicKey = getPublicKey(hexToBytes(privateKeyHex));
|
|
115
113
|
const base = { id, privateKey: privateKeyHex, publicKey, createdBy };
|
|
116
|
-
return parentEpoch !==
|
|
114
|
+
return parentEpoch !== void 0 ? { ...base, parentEpoch } : base;
|
|
117
115
|
}
|
|
118
116
|
function createEpochStore(initialEpoch) {
|
|
119
|
-
const epochs = new Map;
|
|
120
|
-
const keysByPublicKey = new Map;
|
|
117
|
+
const epochs = /* @__PURE__ */ new Map();
|
|
118
|
+
const keysByPublicKey = /* @__PURE__ */ new Map();
|
|
121
119
|
epochs.set(initialEpoch.id, initialEpoch);
|
|
122
120
|
keysByPublicKey.set(initialEpoch.publicKey, hexToBytes(initialEpoch.privateKey));
|
|
123
121
|
return { epochs, keysByPublicKey, currentEpochId: initialEpoch.id };
|
|
@@ -127,7 +125,14 @@ function addEpoch(store, epoch) {
|
|
|
127
125
|
store.keysByPublicKey.set(epoch.publicKey, hexToBytes(epoch.privateKey));
|
|
128
126
|
}
|
|
129
127
|
function hydrateEpochStore(snapshot) {
|
|
130
|
-
const [firstEpoch, ...remainingEpochs] = snapshot.epochs.map(
|
|
128
|
+
const [firstEpoch, ...remainingEpochs] = snapshot.epochs.map(
|
|
129
|
+
(epoch) => createEpochKey(
|
|
130
|
+
EpochId(epoch.id),
|
|
131
|
+
epoch.privateKey,
|
|
132
|
+
epoch.createdBy,
|
|
133
|
+
epoch.parentEpoch !== void 0 ? EpochId(epoch.parentEpoch) : void 0
|
|
134
|
+
)
|
|
135
|
+
);
|
|
131
136
|
if (!firstEpoch) {
|
|
132
137
|
throw new Error("Epoch snapshot must contain at least one epoch");
|
|
133
138
|
}
|
|
@@ -143,9 +148,16 @@ function createEpochStoreFromInputs(epochKeys, options = {}) {
|
|
|
143
148
|
throw new Error("Epoch input must contain at least one key");
|
|
144
149
|
}
|
|
145
150
|
const createdBy = options.createdBy ?? "";
|
|
146
|
-
const epochs = epochKeys.map(
|
|
151
|
+
const epochs = epochKeys.map(
|
|
152
|
+
(epochKey, index) => createEpochKey(
|
|
153
|
+
epochKey.epochId,
|
|
154
|
+
epochKey.key,
|
|
155
|
+
createdBy,
|
|
156
|
+
index > 0 ? epochKeys[index - 1].epochId : void 0
|
|
157
|
+
)
|
|
158
|
+
);
|
|
147
159
|
const store = createEpochStore(epochs[0]);
|
|
148
|
-
for (let i = 1;i < epochs.length; i++) {
|
|
160
|
+
for (let i = 1; i < epochs.length; i++) {
|
|
149
161
|
addEpoch(store, epochs[i]);
|
|
150
162
|
}
|
|
151
163
|
store.currentEpochId = epochs[epochs.length - 1].id;
|
|
@@ -172,7 +184,7 @@ function serializeEpochStore(store) {
|
|
|
172
184
|
id: epoch.id,
|
|
173
185
|
privateKey: epoch.privateKey,
|
|
174
186
|
createdBy: epoch.createdBy,
|
|
175
|
-
...epoch.parentEpoch !==
|
|
187
|
+
...epoch.parentEpoch !== void 0 ? { parentEpoch: epoch.parentEpoch } : {}
|
|
176
188
|
})),
|
|
177
189
|
currentEpochId: store.currentEpochId
|
|
178
190
|
};
|
|
@@ -197,49 +209,61 @@ var RuntimeConfigSchema = Schema2.Struct({
|
|
|
197
209
|
epochKeys: Schema2.optional(Schema2.Array(EpochKeyInputSchema))
|
|
198
210
|
});
|
|
199
211
|
function resolveRuntimeConfig(source) {
|
|
200
|
-
return Schema2.decodeUnknownEffect(RuntimeConfigSchema)(source).pipe(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
212
|
+
return Schema2.decodeUnknownEffect(RuntimeConfigSchema)(source).pipe(
|
|
213
|
+
Effect.map((config) => ({
|
|
214
|
+
relays: [...config.relays],
|
|
215
|
+
privateKey: config.privateKey,
|
|
216
|
+
epochKeys: config.epochKeys?.map((ek) => ({ epochId: EpochId(ek.epochId), key: ek.key })),
|
|
217
|
+
dbName: DatabaseName(config.dbName ?? "tablinum")
|
|
218
|
+
})),
|
|
219
|
+
Effect.mapError(
|
|
220
|
+
(error) => new ValidationError({
|
|
221
|
+
message: `Invalid Tablinum configuration: ${error.message}`
|
|
222
|
+
})
|
|
223
|
+
)
|
|
224
|
+
);
|
|
208
225
|
}
|
|
209
226
|
|
|
210
227
|
// src/services/Config.ts
|
|
211
228
|
import { ServiceMap } from "effect";
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
229
|
+
var Config = class extends ServiceMap.Service()("tablinum/Config") {
|
|
230
|
+
};
|
|
215
231
|
|
|
216
232
|
// src/services/Tablinum.ts
|
|
217
233
|
import { ServiceMap as ServiceMap2 } from "effect";
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
234
|
+
var Tablinum = class extends ServiceMap2.Service()(
|
|
235
|
+
"tablinum/Tablinum"
|
|
236
|
+
) {
|
|
237
|
+
};
|
|
221
238
|
|
|
222
239
|
// src/layers/TablinumLive.ts
|
|
223
240
|
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";
|
|
224
241
|
|
|
225
242
|
// src/crud/watch.ts
|
|
226
243
|
import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
|
|
227
|
-
function watchCollection(ctx, storage, collectionName, filter,
|
|
244
|
+
function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
|
|
228
245
|
const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
|
|
229
246
|
const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
|
|
230
|
-
return
|
|
247
|
+
return mapRecord2 ? filtered.map(mapRecord2) : filtered;
|
|
231
248
|
});
|
|
232
|
-
const changes = Stream.fromPubSub(ctx.pubsub).pipe(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
249
|
+
const changes = Stream.fromPubSub(ctx.pubsub).pipe(
|
|
250
|
+
Stream.filter((event) => event.collection === collectionName),
|
|
251
|
+
Stream.mapEffect(
|
|
252
|
+
() => Effect2.gen(function* () {
|
|
253
|
+
const replaying = yield* Ref.get(ctx.replayingRef);
|
|
254
|
+
if (replaying) return void 0;
|
|
255
|
+
return yield* query();
|
|
256
|
+
})
|
|
257
|
+
),
|
|
258
|
+
Stream.filter((result) => result !== void 0)
|
|
259
|
+
);
|
|
260
|
+
return Stream.unwrap(
|
|
261
|
+
Effect2.gen(function* () {
|
|
262
|
+
yield* Effect2.sleep(0);
|
|
263
|
+
const initial = yield* query();
|
|
264
|
+
return Stream.concat(Stream.make(initial), changes);
|
|
265
|
+
})
|
|
266
|
+
);
|
|
243
267
|
}
|
|
244
268
|
function notifyChange(ctx, event) {
|
|
245
269
|
return PubSub.publish(ctx.pubsub, event).pipe(Effect2.asVoid);
|
|
@@ -262,12 +286,9 @@ import { Effect as Effect3 } from "effect";
|
|
|
262
286
|
|
|
263
287
|
// src/storage/lww.ts
|
|
264
288
|
function resolveWinner(existing, incoming) {
|
|
265
|
-
if (existing === null)
|
|
266
|
-
|
|
267
|
-
if (incoming.createdAt
|
|
268
|
-
return incoming;
|
|
269
|
-
if (incoming.createdAt < existing.createdAt)
|
|
270
|
-
return existing;
|
|
289
|
+
if (existing === null) return incoming;
|
|
290
|
+
if (incoming.createdAt > existing.createdAt) return incoming;
|
|
291
|
+
if (incoming.createdAt < existing.createdAt) return existing;
|
|
271
292
|
return incoming.id < existing.id ? incoming : existing;
|
|
272
293
|
}
|
|
273
294
|
|
|
@@ -334,8 +355,7 @@ function applyEvent(storage, event) {
|
|
|
334
355
|
};
|
|
335
356
|
const incomingMeta = { id: event.id, createdAt: event.createdAt };
|
|
336
357
|
const winner = resolveWinner(existingMeta, incomingMeta);
|
|
337
|
-
if (winner.id !== event.id)
|
|
338
|
-
return false;
|
|
358
|
+
if (winner.id !== event.id) return false;
|
|
339
359
|
}
|
|
340
360
|
if (existing && event.kind === "u") {
|
|
341
361
|
yield* storage.putRecord(event.collection, deepMerge(existing, buildRecord(event)));
|
|
@@ -351,10 +371,11 @@ function rebuild(storage, collections) {
|
|
|
351
371
|
yield* storage.clearRecords(col);
|
|
352
372
|
}
|
|
353
373
|
const allEvents = yield* storage.getAllEvents();
|
|
354
|
-
const sorted = [...allEvents].sort(
|
|
374
|
+
const sorted = [...allEvents].sort(
|
|
375
|
+
(a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1)
|
|
376
|
+
);
|
|
355
377
|
for (const event of sorted) {
|
|
356
|
-
if (event.data === null && event.kind !== "d")
|
|
357
|
-
continue;
|
|
378
|
+
if (event.data === null && event.kind !== "d") continue;
|
|
358
379
|
yield* applyEvent(storage, event);
|
|
359
380
|
}
|
|
360
381
|
});
|
|
@@ -401,15 +422,22 @@ function buildStructSchema(def, options = {}) {
|
|
|
401
422
|
}
|
|
402
423
|
for (const [name, fieldDef] of Object.entries(def.fields)) {
|
|
403
424
|
const fieldSchema = fieldDefToSchema(fieldDef);
|
|
404
|
-
schemaFields[name] = options.allOptional ? Schema3.optionalKey(fieldSchema) : fieldSchema;
|
|
425
|
+
schemaFields[name] = options.allOptional || fieldDef.isOptional ? Schema3.optionalKey(fieldSchema) : fieldSchema;
|
|
405
426
|
}
|
|
406
427
|
return Schema3.Struct(schemaFields);
|
|
407
428
|
}
|
|
408
429
|
function buildValidator(collectionName, def) {
|
|
409
430
|
const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
410
|
-
return (input) => decode(input).pipe(
|
|
411
|
-
|
|
412
|
-
|
|
431
|
+
return (input) => decode(input).pipe(
|
|
432
|
+
Effect4.map(
|
|
433
|
+
(result) => result
|
|
434
|
+
),
|
|
435
|
+
Effect4.mapError(
|
|
436
|
+
(e) => new ValidationError({
|
|
437
|
+
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
438
|
+
})
|
|
439
|
+
)
|
|
440
|
+
);
|
|
413
441
|
}
|
|
414
442
|
function buildPartialValidator(collectionName, def) {
|
|
415
443
|
const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
@@ -421,15 +449,20 @@ function buildPartialValidator(collectionName, def) {
|
|
|
421
449
|
}
|
|
422
450
|
const record = input;
|
|
423
451
|
const unknownField = Object.keys(record).find((key) => !Object.hasOwn(def.fields, key));
|
|
424
|
-
if (unknownField !==
|
|
452
|
+
if (unknownField !== void 0) {
|
|
425
453
|
return yield* new ValidationError({
|
|
426
454
|
message: `Unknown field "${unknownField}" in collection "${collectionName}"`,
|
|
427
455
|
field: unknownField
|
|
428
456
|
});
|
|
429
457
|
}
|
|
430
|
-
return yield* decode(record).pipe(
|
|
431
|
-
|
|
432
|
-
|
|
458
|
+
return yield* decode(record).pipe(
|
|
459
|
+
Effect4.map((result) => result),
|
|
460
|
+
Effect4.mapError(
|
|
461
|
+
(e) => new ValidationError({
|
|
462
|
+
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
463
|
+
})
|
|
464
|
+
)
|
|
465
|
+
);
|
|
433
466
|
});
|
|
434
467
|
}
|
|
435
468
|
|
|
@@ -440,16 +473,14 @@ import { Effect as Effect6, Option as Option3, References } from "effect";
|
|
|
440
473
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
441
474
|
function toBase64url(bytes) {
|
|
442
475
|
let result = "";
|
|
443
|
-
for (let i = 0;i < bytes.length; i += 3) {
|
|
476
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
444
477
|
const b0 = bytes[i];
|
|
445
478
|
const b1 = bytes[i + 1] ?? 0;
|
|
446
479
|
const b2 = bytes[i + 2] ?? 0;
|
|
447
480
|
result += alphabet[b0 >> 2];
|
|
448
481
|
result += alphabet[(b0 & 3) << 4 | b1 >> 4];
|
|
449
|
-
if (i + 1 < bytes.length)
|
|
450
|
-
|
|
451
|
-
if (i + 2 < bytes.length)
|
|
452
|
-
result += alphabet[b2 & 63];
|
|
482
|
+
if (i + 1 < bytes.length) result += alphabet[(b1 & 15) << 2 | b2 >> 6];
|
|
483
|
+
if (i + 2 < bytes.length) result += alphabet[b2 & 63];
|
|
453
484
|
}
|
|
454
485
|
return result;
|
|
455
486
|
}
|
|
@@ -494,16 +525,28 @@ function executeQuery(ctx, plan) {
|
|
|
494
525
|
if (plan.indexQuery && ctx.def.indices.includes(plan.indexQuery.field)) {
|
|
495
526
|
if (plan.indexQuery.type === "value") {
|
|
496
527
|
results = [
|
|
497
|
-
...yield* ctx.storage.getByIndex(
|
|
528
|
+
...yield* ctx.storage.getByIndex(
|
|
529
|
+
ctx.collectionName,
|
|
530
|
+
plan.indexQuery.field,
|
|
531
|
+
plan.indexQuery.range
|
|
532
|
+
)
|
|
498
533
|
];
|
|
499
534
|
} else {
|
|
500
535
|
results = [
|
|
501
|
-
...yield* ctx.storage.getByIndexRange(
|
|
536
|
+
...yield* ctx.storage.getByIndexRange(
|
|
537
|
+
ctx.collectionName,
|
|
538
|
+
plan.indexQuery.field,
|
|
539
|
+
plan.indexQuery.range
|
|
540
|
+
)
|
|
502
541
|
];
|
|
503
542
|
}
|
|
504
543
|
} else if (plan.orderBy && ctx.def.indices.includes(plan.orderBy.field) && plan.filters.length === 0) {
|
|
505
544
|
results = [
|
|
506
|
-
...yield* ctx.storage.getAllSorted(
|
|
545
|
+
...yield* ctx.storage.getAllSorted(
|
|
546
|
+
ctx.collectionName,
|
|
547
|
+
plan.orderBy.field,
|
|
548
|
+
plan.orderBy.direction === "desc" ? "prev" : "next"
|
|
549
|
+
)
|
|
507
550
|
];
|
|
508
551
|
} else {
|
|
509
552
|
results = [...yield* ctx.storage.getAllRecords(ctx.collectionName)];
|
|
@@ -527,7 +570,7 @@ function executeQuery(ctx, plan) {
|
|
|
527
570
|
if (plan.offset) {
|
|
528
571
|
results = results.slice(plan.offset);
|
|
529
572
|
}
|
|
530
|
-
if (plan.limit !== null && plan.limit !==
|
|
573
|
+
if (plan.limit !== null && plan.limit !== void 0) {
|
|
531
574
|
results = results.slice(0, plan.limit);
|
|
532
575
|
}
|
|
533
576
|
return results.map(ctx.mapRecord);
|
|
@@ -535,16 +578,23 @@ function executeQuery(ctx, plan) {
|
|
|
535
578
|
}
|
|
536
579
|
function watchQuery(ctx, plan) {
|
|
537
580
|
const query = () => executeQuery(ctx, plan);
|
|
538
|
-
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
581
|
+
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
582
|
+
Stream2.filter((event) => event.collection === ctx.collectionName),
|
|
583
|
+
Stream2.mapEffect(
|
|
584
|
+
() => Effect5.gen(function* () {
|
|
585
|
+
const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
|
|
586
|
+
if (replaying) return void 0;
|
|
587
|
+
return yield* query();
|
|
588
|
+
})
|
|
589
|
+
),
|
|
590
|
+
Stream2.filter((result) => result !== void 0)
|
|
591
|
+
);
|
|
592
|
+
return Stream2.unwrap(
|
|
593
|
+
Effect5.gen(function* () {
|
|
594
|
+
const initial = yield* query();
|
|
595
|
+
return Stream2.concat(Stream2.make(initial), changes);
|
|
596
|
+
})
|
|
597
|
+
);
|
|
548
598
|
}
|
|
549
599
|
function makeQueryBuilder(ctx, plan) {
|
|
550
600
|
return {
|
|
@@ -566,15 +616,18 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
566
616
|
offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
|
|
567
617
|
limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
|
|
568
618
|
get: () => executeQuery(ctx, plan),
|
|
569
|
-
first: () => Effect5.map(
|
|
619
|
+
first: () => Effect5.map(
|
|
620
|
+
executeQuery(ctx, { ...plan, limit: 1 }),
|
|
621
|
+
(results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
|
|
622
|
+
),
|
|
570
623
|
count: () => Effect5.map(executeQuery(ctx, plan), (results) => results.length),
|
|
571
624
|
watch: () => watchQuery(ctx, plan)
|
|
572
625
|
};
|
|
573
626
|
}
|
|
574
|
-
function createWhereClause(storage, watchCtx, collectionName, def, fieldName,
|
|
575
|
-
const ctx = { storage, watchCtx, collectionName, def, mapRecord };
|
|
627
|
+
function createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord2) {
|
|
628
|
+
const ctx = { storage, watchCtx, collectionName, def, mapRecord: mapRecord2 };
|
|
576
629
|
const fieldDef = def.fields[fieldName];
|
|
577
|
-
const isIndexed = def.indices.includes(fieldName) && fieldDef !== null && fieldDef !==
|
|
630
|
+
const isIndexed = def.indices.includes(fieldName) && fieldDef !== null && fieldDef !== void 0 && fieldDef.kind !== "boolean";
|
|
578
631
|
const withFilter = (filterFn, indexQuery) => {
|
|
579
632
|
const plan = {
|
|
580
633
|
...emptyPlan(),
|
|
@@ -585,30 +638,51 @@ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, ma
|
|
|
585
638
|
return makeQueryBuilder(ctx, plan);
|
|
586
639
|
};
|
|
587
640
|
return {
|
|
588
|
-
equals: (value) => withFilter(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
641
|
+
equals: (value) => withFilter(
|
|
642
|
+
(r) => r[fieldName] === value,
|
|
643
|
+
isIndexed ? { field: fieldName, range: value, type: "value" } : void 0
|
|
644
|
+
),
|
|
645
|
+
above: (value) => withFilter(
|
|
646
|
+
(r) => r[fieldName] > value,
|
|
647
|
+
isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, true), type: "range" } : void 0
|
|
648
|
+
),
|
|
649
|
+
aboveOrEqual: (value) => withFilter(
|
|
650
|
+
(r) => r[fieldName] >= value,
|
|
651
|
+
isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, false), type: "range" } : void 0
|
|
652
|
+
),
|
|
653
|
+
below: (value) => withFilter(
|
|
654
|
+
(r) => r[fieldName] < value,
|
|
655
|
+
isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, true), type: "range" } : void 0
|
|
656
|
+
),
|
|
657
|
+
belowOrEqual: (value) => withFilter(
|
|
658
|
+
(r) => r[fieldName] <= value,
|
|
659
|
+
isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, false), type: "range" } : void 0
|
|
660
|
+
),
|
|
593
661
|
between: (lower, upper, options) => {
|
|
594
662
|
const includeLower = options?.includeLower ?? true;
|
|
595
663
|
const includeUpper = options?.includeUpper ?? true;
|
|
596
|
-
return withFilter(
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
664
|
+
return withFilter(
|
|
665
|
+
(r) => {
|
|
666
|
+
const v = r[fieldName];
|
|
667
|
+
const aboveLower = includeLower ? v >= lower : v > lower;
|
|
668
|
+
const belowUpper = includeUpper ? v <= upper : v < upper;
|
|
669
|
+
return aboveLower && belowUpper;
|
|
670
|
+
},
|
|
671
|
+
isIndexed ? {
|
|
672
|
+
field: fieldName,
|
|
673
|
+
range: IDBKeyRange.bound(lower, upper, !includeLower, !includeUpper),
|
|
674
|
+
type: "range"
|
|
675
|
+
} : void 0
|
|
676
|
+
);
|
|
677
|
+
},
|
|
678
|
+
startsWith: (prefix) => withFilter(
|
|
679
|
+
(r) => typeof r[fieldName] === "string" && r[fieldName].startsWith(prefix),
|
|
680
|
+
isIndexed ? {
|
|
602
681
|
field: fieldName,
|
|
603
|
-
range: IDBKeyRange.bound(
|
|
682
|
+
range: IDBKeyRange.bound(prefix, prefix + "\uFFFF", false, false),
|
|
604
683
|
type: "range"
|
|
605
|
-
} :
|
|
606
|
-
|
|
607
|
-
startsWith: (prefix) => withFilter((r) => typeof r[fieldName] === "string" && r[fieldName].startsWith(prefix), isIndexed ? {
|
|
608
|
-
field: fieldName,
|
|
609
|
-
range: IDBKeyRange.bound(prefix, prefix + "", false, false),
|
|
610
|
-
type: "range"
|
|
611
|
-
} : undefined),
|
|
684
|
+
} : void 0
|
|
685
|
+
),
|
|
612
686
|
anyOf: (values) => {
|
|
613
687
|
const set = new Set(values);
|
|
614
688
|
return withFilter((r) => set.has(r[fieldName]));
|
|
@@ -619,8 +693,8 @@ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, ma
|
|
|
619
693
|
}
|
|
620
694
|
};
|
|
621
695
|
}
|
|
622
|
-
function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName,
|
|
623
|
-
const ctx = { storage, watchCtx, collectionName, def, mapRecord };
|
|
696
|
+
function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord2) {
|
|
697
|
+
const ctx = { storage, watchCtx, collectionName, def, mapRecord: mapRecord2 };
|
|
624
698
|
const plan = {
|
|
625
699
|
...emptyPlan(),
|
|
626
700
|
orderBy: { field: fieldName, direction: "asc" }
|
|
@@ -645,7 +719,7 @@ function replayState(recordId, events, stopAtId) {
|
|
|
645
719
|
state = deepMerge(state, e.data);
|
|
646
720
|
}
|
|
647
721
|
}
|
|
648
|
-
if (stopAtId !==
|
|
722
|
+
if (stopAtId !== void 0 && e.id === stopAtId) {
|
|
649
723
|
break;
|
|
650
724
|
}
|
|
651
725
|
}
|
|
@@ -663,8 +737,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
|
663
737
|
function pruneEvents(storage, collection2, recordId, retention) {
|
|
664
738
|
return Effect6.gen(function* () {
|
|
665
739
|
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
666
|
-
if (events.length <= retention)
|
|
667
|
-
return;
|
|
740
|
+
if (events.length <= retention) return;
|
|
668
741
|
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
669
742
|
const retained = sorted.slice(0, retention);
|
|
670
743
|
const toStrip = sorted.slice(retention);
|
|
@@ -673,8 +746,7 @@ function pruneEvents(storage, collection2, recordId, retention) {
|
|
|
673
746
|
yield* promoteToSnapshot(storage, collection2, recordId, oldestRetained, sorted);
|
|
674
747
|
}
|
|
675
748
|
for (const e of toStrip) {
|
|
676
|
-
if (e.data !== null)
|
|
677
|
-
yield* storage.stripEventData(e.id);
|
|
749
|
+
if (e.data !== null) yield* storage.stripEventData(e.id);
|
|
678
750
|
}
|
|
679
751
|
});
|
|
680
752
|
}
|
|
@@ -688,8 +760,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
688
760
|
const commitEvent = (event) => Effect6.gen(function* () {
|
|
689
761
|
yield* storage.putEvent(event);
|
|
690
762
|
yield* applyEvent(storage, event);
|
|
691
|
-
if (onWrite)
|
|
692
|
-
yield* onWrite(event);
|
|
763
|
+
if (onWrite) yield* onWrite(event);
|
|
693
764
|
yield* notifyChange(watchCtx, {
|
|
694
765
|
collection: collectionName,
|
|
695
766
|
recordId: event.recordId,
|
|
@@ -697,70 +768,84 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
697
768
|
});
|
|
698
769
|
});
|
|
699
770
|
const handle = {
|
|
700
|
-
add: (data) => withLog(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
recordId: id,
|
|
708
|
-
kind: "c",
|
|
709
|
-
data: fullRecord,
|
|
710
|
-
createdAt: Date.now(),
|
|
711
|
-
author: localAuthor
|
|
712
|
-
};
|
|
713
|
-
yield* commitEvent(event);
|
|
714
|
-
yield* Effect6.logDebug("Record added", { collection: collectionName, recordId: id, data: fullRecord });
|
|
715
|
-
return id;
|
|
716
|
-
})),
|
|
717
|
-
update: (id, data) => withLog(Effect6.gen(function* () {
|
|
718
|
-
const existing = yield* storage.getRecord(collectionName, id);
|
|
719
|
-
if (!existing || existing._d) {
|
|
720
|
-
return yield* new NotFoundError({
|
|
771
|
+
add: (data) => withLog(
|
|
772
|
+
Effect6.gen(function* () {
|
|
773
|
+
const id = uuidv7();
|
|
774
|
+
const fullRecord = { id, ...data };
|
|
775
|
+
yield* validator(fullRecord);
|
|
776
|
+
const event = {
|
|
777
|
+
id: makeEventId(),
|
|
721
778
|
collection: collectionName,
|
|
722
|
-
id
|
|
779
|
+
recordId: id,
|
|
780
|
+
kind: "c",
|
|
781
|
+
data: fullRecord,
|
|
782
|
+
createdAt: Date.now(),
|
|
783
|
+
author: localAuthor
|
|
784
|
+
};
|
|
785
|
+
yield* commitEvent(event);
|
|
786
|
+
yield* Effect6.logDebug("Record added", {
|
|
787
|
+
collection: collectionName,
|
|
788
|
+
recordId: id,
|
|
789
|
+
data: fullRecord
|
|
723
790
|
});
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
delete: (id) => withLog(Effect6.gen(function* () {
|
|
744
|
-
const existing = yield* storage.getRecord(collectionName, id);
|
|
745
|
-
if (!existing || existing._d) {
|
|
746
|
-
return yield* new NotFoundError({
|
|
791
|
+
return id;
|
|
792
|
+
})
|
|
793
|
+
),
|
|
794
|
+
update: (id, data) => withLog(
|
|
795
|
+
Effect6.gen(function* () {
|
|
796
|
+
const existing = yield* storage.getRecord(collectionName, id);
|
|
797
|
+
if (!existing || existing._d) {
|
|
798
|
+
return yield* new NotFoundError({
|
|
799
|
+
collection: collectionName,
|
|
800
|
+
id
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
yield* partialValidator(data);
|
|
804
|
+
const { _d, _u, _a, _e, ...existingFields } = existing;
|
|
805
|
+
const merged = { ...existingFields, ...data, id };
|
|
806
|
+
yield* validator(merged);
|
|
807
|
+
const diff = deepDiff(existingFields, merged);
|
|
808
|
+
const event = {
|
|
809
|
+
id: makeEventId(),
|
|
747
810
|
collection: collectionName,
|
|
748
|
-
id
|
|
811
|
+
recordId: id,
|
|
812
|
+
kind: "u",
|
|
813
|
+
data: diff ?? { id },
|
|
814
|
+
createdAt: Date.now(),
|
|
815
|
+
author: localAuthor
|
|
816
|
+
};
|
|
817
|
+
yield* commitEvent(event);
|
|
818
|
+
yield* Effect6.logDebug("Record updated", {
|
|
819
|
+
collection: collectionName,
|
|
820
|
+
recordId: id,
|
|
821
|
+
data: diff
|
|
749
822
|
});
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
823
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
824
|
+
})
|
|
825
|
+
),
|
|
826
|
+
delete: (id) => withLog(
|
|
827
|
+
Effect6.gen(function* () {
|
|
828
|
+
const existing = yield* storage.getRecord(collectionName, id);
|
|
829
|
+
if (!existing || existing._d) {
|
|
830
|
+
return yield* new NotFoundError({
|
|
831
|
+
collection: collectionName,
|
|
832
|
+
id
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
const event = {
|
|
836
|
+
id: makeEventId(),
|
|
837
|
+
collection: collectionName,
|
|
838
|
+
recordId: id,
|
|
839
|
+
kind: "d",
|
|
840
|
+
data: null,
|
|
841
|
+
createdAt: Date.now(),
|
|
842
|
+
author: localAuthor
|
|
843
|
+
};
|
|
844
|
+
yield* commitEvent(event);
|
|
845
|
+
yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
846
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
847
|
+
})
|
|
848
|
+
),
|
|
764
849
|
undo: (id) => Effect6.gen(function* () {
|
|
765
850
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
766
851
|
if (!existing) {
|
|
@@ -801,15 +886,29 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
801
886
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
802
887
|
}),
|
|
803
888
|
count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
804
|
-
watch: () => watchCollection(watchCtx, storage, collectionName,
|
|
805
|
-
where: (fieldName) => createWhereClause(
|
|
806
|
-
|
|
889
|
+
watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
|
|
890
|
+
where: (fieldName) => createWhereClause(
|
|
891
|
+
storage,
|
|
892
|
+
watchCtx,
|
|
893
|
+
collectionName,
|
|
894
|
+
def,
|
|
895
|
+
fieldName,
|
|
896
|
+
mapRecord
|
|
897
|
+
),
|
|
898
|
+
orderBy: (fieldName) => createOrderByBuilder(
|
|
899
|
+
storage,
|
|
900
|
+
watchCtx,
|
|
901
|
+
collectionName,
|
|
902
|
+
def,
|
|
903
|
+
fieldName,
|
|
904
|
+
mapRecord
|
|
905
|
+
)
|
|
807
906
|
};
|
|
808
907
|
return handle;
|
|
809
908
|
}
|
|
810
909
|
|
|
811
910
|
// src/sync/sync-service.ts
|
|
812
|
-
import { Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
911
|
+
import { Duration, Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
813
912
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
814
913
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
815
914
|
|
|
@@ -825,8 +924,7 @@ var Mode = {
|
|
|
825
924
|
Fingerprint: 1,
|
|
826
925
|
IdList: 2
|
|
827
926
|
};
|
|
828
|
-
|
|
829
|
-
class WrappedBuffer {
|
|
927
|
+
var WrappedBuffer = class {
|
|
830
928
|
constructor(buffer) {
|
|
831
929
|
this._raw = new Uint8Array(buffer || 512);
|
|
832
930
|
this.length = buffer ? buffer.length : 0;
|
|
@@ -838,10 +936,8 @@ class WrappedBuffer {
|
|
|
838
936
|
return this._raw.byteLength;
|
|
839
937
|
}
|
|
840
938
|
extend(buf) {
|
|
841
|
-
if (buf._raw)
|
|
842
|
-
|
|
843
|
-
if (typeof buf.length !== "number")
|
|
844
|
-
throw Error("bad length");
|
|
939
|
+
if (buf._raw) buf = buf.unwrap();
|
|
940
|
+
if (typeof buf.length !== "number") throw Error("bad length");
|
|
845
941
|
const targetSize = buf.length + this.length;
|
|
846
942
|
if (this.capacity < targetSize) {
|
|
847
943
|
const oldRaw = this._raw;
|
|
@@ -864,42 +960,36 @@ class WrappedBuffer {
|
|
|
864
960
|
this.length -= n;
|
|
865
961
|
return firstSubarray;
|
|
866
962
|
}
|
|
867
|
-
}
|
|
963
|
+
};
|
|
868
964
|
function decodeVarInt(buf) {
|
|
869
965
|
let res = 0;
|
|
870
|
-
while (
|
|
871
|
-
if (buf.length === 0)
|
|
872
|
-
throw Error("parse ends prematurely");
|
|
966
|
+
while (1) {
|
|
967
|
+
if (buf.length === 0) throw Error("parse ends prematurely");
|
|
873
968
|
let byte = buf.shift();
|
|
874
969
|
res = res << 7 | byte & 127;
|
|
875
|
-
if ((byte & 128) === 0)
|
|
876
|
-
break;
|
|
970
|
+
if ((byte & 128) === 0) break;
|
|
877
971
|
}
|
|
878
972
|
return res;
|
|
879
973
|
}
|
|
880
974
|
function encodeVarInt(n) {
|
|
881
|
-
if (n === 0)
|
|
882
|
-
return new WrappedBuffer([0]);
|
|
975
|
+
if (n === 0) return new WrappedBuffer([0]);
|
|
883
976
|
let o = [];
|
|
884
977
|
while (n !== 0) {
|
|
885
978
|
o.push(n & 127);
|
|
886
979
|
n >>>= 7;
|
|
887
980
|
}
|
|
888
981
|
o.reverse();
|
|
889
|
-
for (let i = 0;i < o.length - 1; i++)
|
|
890
|
-
o[i] |= 128;
|
|
982
|
+
for (let i = 0; i < o.length - 1; i++) o[i] |= 128;
|
|
891
983
|
return new WrappedBuffer(o);
|
|
892
984
|
}
|
|
893
985
|
function getByte(buf) {
|
|
894
986
|
return getBytes(buf, 1)[0];
|
|
895
987
|
}
|
|
896
988
|
function getBytes(buf, n) {
|
|
897
|
-
if (buf.length < n)
|
|
898
|
-
throw Error("parse ends prematurely");
|
|
989
|
+
if (buf.length < n) throw Error("parse ends prematurely");
|
|
899
990
|
return buf.shiftN(n);
|
|
900
991
|
}
|
|
901
|
-
|
|
902
|
-
class Accumulator {
|
|
992
|
+
var Accumulator = class {
|
|
903
993
|
constructor() {
|
|
904
994
|
this.setToZero();
|
|
905
995
|
if (typeof window === "undefined") {
|
|
@@ -916,15 +1006,14 @@ class Accumulator {
|
|
|
916
1006
|
let currCarry = 0, nextCarry = 0;
|
|
917
1007
|
let p = new DataView(this.buf.buffer);
|
|
918
1008
|
let po = new DataView(otherBuf.buffer);
|
|
919
|
-
for (let i = 0;i < 8; i++) {
|
|
1009
|
+
for (let i = 0; i < 8; i++) {
|
|
920
1010
|
let offset = i * 4;
|
|
921
1011
|
let orig = p.getUint32(offset, true);
|
|
922
1012
|
let otherV = po.getUint32(offset, true);
|
|
923
1013
|
let next = orig;
|
|
924
1014
|
next += currCarry;
|
|
925
1015
|
next += otherV;
|
|
926
|
-
if (next > 4294967295)
|
|
927
|
-
nextCarry = 1;
|
|
1016
|
+
if (next > 4294967295) nextCarry = 1;
|
|
928
1017
|
p.setUint32(offset, next & 4294967295, true);
|
|
929
1018
|
currCarry = nextCarry;
|
|
930
1019
|
nextCarry = 0;
|
|
@@ -932,7 +1021,7 @@ class Accumulator {
|
|
|
932
1021
|
}
|
|
933
1022
|
negate() {
|
|
934
1023
|
let p = new DataView(this.buf.buffer);
|
|
935
|
-
for (let i = 0;i < 8; i++) {
|
|
1024
|
+
for (let i = 0; i < 8; i++) {
|
|
936
1025
|
let offset = i * 4;
|
|
937
1026
|
p.setUint32(offset, ~p.getUint32(offset, true));
|
|
938
1027
|
}
|
|
@@ -941,33 +1030,29 @@ class Accumulator {
|
|
|
941
1030
|
this.add(one);
|
|
942
1031
|
}
|
|
943
1032
|
async getFingerprint(n) {
|
|
944
|
-
let input = new WrappedBuffer;
|
|
1033
|
+
let input = new WrappedBuffer();
|
|
945
1034
|
input.extend(this.buf);
|
|
946
1035
|
input.extend(encodeVarInt(n));
|
|
947
1036
|
let hash = await this.sha256(input.unwrap());
|
|
948
1037
|
return hash.subarray(0, FINGERPRINT_SIZE);
|
|
949
1038
|
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
class NegentropyStorageVector {
|
|
1039
|
+
};
|
|
1040
|
+
var NegentropyStorageVector = class {
|
|
953
1041
|
constructor() {
|
|
954
1042
|
this.items = [];
|
|
955
1043
|
this.sealed = false;
|
|
956
1044
|
}
|
|
957
1045
|
insert(timestamp, id) {
|
|
958
|
-
if (this.sealed)
|
|
959
|
-
throw Error("already sealed");
|
|
1046
|
+
if (this.sealed) throw Error("already sealed");
|
|
960
1047
|
id = loadInputBuffer(id);
|
|
961
|
-
if (id.byteLength !== ID_SIZE)
|
|
962
|
-
throw Error("bad id size for added item");
|
|
1048
|
+
if (id.byteLength !== ID_SIZE) throw Error("bad id size for added item");
|
|
963
1049
|
this.items.push({ timestamp, id });
|
|
964
1050
|
}
|
|
965
1051
|
seal() {
|
|
966
|
-
if (this.sealed)
|
|
967
|
-
throw Error("already sealed");
|
|
1052
|
+
if (this.sealed) throw Error("already sealed");
|
|
968
1053
|
this.sealed = true;
|
|
969
1054
|
this.items.sort(itemCompare);
|
|
970
|
-
for (let i = 1;i < this.items.length; i++) {
|
|
1055
|
+
for (let i = 1; i < this.items.length; i++) {
|
|
971
1056
|
if (itemCompare(this.items[i - 1], this.items[i]) === 0)
|
|
972
1057
|
throw Error("duplicate item inserted");
|
|
973
1058
|
}
|
|
@@ -981,16 +1066,14 @@ class NegentropyStorageVector {
|
|
|
981
1066
|
}
|
|
982
1067
|
getItem(i) {
|
|
983
1068
|
this._checkSealed();
|
|
984
|
-
if (i >= this.items.length)
|
|
985
|
-
throw Error("out of range");
|
|
1069
|
+
if (i >= this.items.length) throw Error("out of range");
|
|
986
1070
|
return this.items[i];
|
|
987
1071
|
}
|
|
988
1072
|
iterate(begin, end, cb) {
|
|
989
1073
|
this._checkSealed();
|
|
990
1074
|
this._checkBounds(begin, end);
|
|
991
|
-
for (let i = begin;i < end; ++i) {
|
|
992
|
-
if (!cb(this.items[i], i))
|
|
993
|
-
break;
|
|
1075
|
+
for (let i = begin; i < end; ++i) {
|
|
1076
|
+
if (!cb(this.items[i], i)) break;
|
|
994
1077
|
}
|
|
995
1078
|
}
|
|
996
1079
|
findLowerBound(begin, end, bound) {
|
|
@@ -999,7 +1082,7 @@ class NegentropyStorageVector {
|
|
|
999
1082
|
return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0);
|
|
1000
1083
|
}
|
|
1001
1084
|
async fingerprint(begin, end) {
|
|
1002
|
-
let out = new Accumulator;
|
|
1085
|
+
let out = new Accumulator();
|
|
1003
1086
|
out.setToZero();
|
|
1004
1087
|
this.iterate(begin, end, (item, i) => {
|
|
1005
1088
|
out.add(item.id);
|
|
@@ -1008,12 +1091,10 @@ class NegentropyStorageVector {
|
|
|
1008
1091
|
return await out.getFingerprint(end - begin);
|
|
1009
1092
|
}
|
|
1010
1093
|
_checkSealed() {
|
|
1011
|
-
if (!this.sealed)
|
|
1012
|
-
throw Error("not sealed");
|
|
1094
|
+
if (!this.sealed) throw Error("not sealed");
|
|
1013
1095
|
}
|
|
1014
1096
|
_checkBounds(begin, end) {
|
|
1015
|
-
if (begin > end || end > this.items.length)
|
|
1016
|
-
throw Error("bad range");
|
|
1097
|
+
if (begin > end || end > this.items.length) throw Error("bad range");
|
|
1017
1098
|
}
|
|
1018
1099
|
_binarySearch(arr, first, last, cmp) {
|
|
1019
1100
|
let count = last - first;
|
|
@@ -1030,12 +1111,10 @@ class NegentropyStorageVector {
|
|
|
1030
1111
|
}
|
|
1031
1112
|
return first;
|
|
1032
1113
|
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
class Negentropy {
|
|
1114
|
+
};
|
|
1115
|
+
var Negentropy = class {
|
|
1036
1116
|
constructor(storage, frameSizeLimit = 0) {
|
|
1037
|
-
if (frameSizeLimit !== 0 && frameSizeLimit < 4096)
|
|
1038
|
-
throw Error("frameSizeLimit too small");
|
|
1117
|
+
if (frameSizeLimit !== 0 && frameSizeLimit < 4096) throw Error("frameSizeLimit too small");
|
|
1039
1118
|
this.storage = storage;
|
|
1040
1119
|
this.frameSizeLimit = frameSizeLimit;
|
|
1041
1120
|
this.lastTimestampIn = 0;
|
|
@@ -1045,10 +1124,9 @@ class Negentropy {
|
|
|
1045
1124
|
return { timestamp, id: id ? id : new Uint8Array(0) };
|
|
1046
1125
|
}
|
|
1047
1126
|
async initiate() {
|
|
1048
|
-
if (this.isInitiator)
|
|
1049
|
-
throw Error("already initiated");
|
|
1127
|
+
if (this.isInitiator) throw Error("already initiated");
|
|
1050
1128
|
this.isInitiator = true;
|
|
1051
|
-
let output = new WrappedBuffer;
|
|
1129
|
+
let output = new WrappedBuffer();
|
|
1052
1130
|
output.extend([PROTOCOL_VERSION]);
|
|
1053
1131
|
await this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output);
|
|
1054
1132
|
return this._renderOutput(output);
|
|
@@ -1060,23 +1138,24 @@ class Negentropy {
|
|
|
1060
1138
|
let haveIds = [], needIds = [];
|
|
1061
1139
|
query = new WrappedBuffer(loadInputBuffer(query));
|
|
1062
1140
|
this.lastTimestampIn = this.lastTimestampOut = 0;
|
|
1063
|
-
let fullOutput = new WrappedBuffer;
|
|
1141
|
+
let fullOutput = new WrappedBuffer();
|
|
1064
1142
|
fullOutput.extend([PROTOCOL_VERSION]);
|
|
1065
1143
|
let protocolVersion = getByte(query);
|
|
1066
1144
|
if (protocolVersion < 96 || protocolVersion > 111)
|
|
1067
1145
|
throw Error("invalid negentropy protocol version byte");
|
|
1068
1146
|
if (protocolVersion !== PROTOCOL_VERSION) {
|
|
1069
1147
|
if (this.isInitiator)
|
|
1070
|
-
throw Error(
|
|
1071
|
-
|
|
1072
|
-
|
|
1148
|
+
throw Error(
|
|
1149
|
+
"unsupported negentropy protocol version requested: " + (protocolVersion - 96)
|
|
1150
|
+
);
|
|
1151
|
+
else return [this._renderOutput(fullOutput), haveIds, needIds];
|
|
1073
1152
|
}
|
|
1074
1153
|
let storageSize = this.storage.size();
|
|
1075
1154
|
let prevBound = this._bound(0);
|
|
1076
1155
|
let prevIndex = 0;
|
|
1077
1156
|
let skip = false;
|
|
1078
1157
|
while (query.length !== 0) {
|
|
1079
|
-
let o = new WrappedBuffer;
|
|
1158
|
+
let o = new WrappedBuffer();
|
|
1080
1159
|
let doSkip = () => {
|
|
1081
1160
|
if (skip) {
|
|
1082
1161
|
skip = false;
|
|
@@ -1102,10 +1181,9 @@ class Negentropy {
|
|
|
1102
1181
|
} else if (mode === Mode.IdList) {
|
|
1103
1182
|
let numIds = decodeVarInt(query);
|
|
1104
1183
|
let theirElems = {};
|
|
1105
|
-
for (let i = 0;i < numIds; i++) {
|
|
1184
|
+
for (let i = 0; i < numIds; i++) {
|
|
1106
1185
|
let e = getBytes(query, ID_SIZE);
|
|
1107
|
-
if (this.isInitiator)
|
|
1108
|
-
theirElems[e] = e;
|
|
1186
|
+
if (this.isInitiator) theirElems[e] = e;
|
|
1109
1187
|
}
|
|
1110
1188
|
if (this.isInitiator) {
|
|
1111
1189
|
skip = true;
|
|
@@ -1124,7 +1202,7 @@ class Negentropy {
|
|
|
1124
1202
|
}
|
|
1125
1203
|
} else {
|
|
1126
1204
|
doSkip();
|
|
1127
|
-
let responseIds = new WrappedBuffer;
|
|
1205
|
+
let responseIds = new WrappedBuffer();
|
|
1128
1206
|
let numResponseIds = 0;
|
|
1129
1207
|
let endBound = currBound;
|
|
1130
1208
|
this.storage.iterate(lower, upper, (item, index) => {
|
|
@@ -1142,7 +1220,7 @@ class Negentropy {
|
|
|
1142
1220
|
o.extend(encodeVarInt(numResponseIds));
|
|
1143
1221
|
o.extend(responseIds);
|
|
1144
1222
|
fullOutput.extend(o);
|
|
1145
|
-
o = new WrappedBuffer;
|
|
1223
|
+
o = new WrappedBuffer();
|
|
1146
1224
|
}
|
|
1147
1225
|
} else {
|
|
1148
1226
|
throw Error("unexpected mode");
|
|
@@ -1180,7 +1258,7 @@ class Negentropy {
|
|
|
1180
1258
|
let itemsPerBucket = Math.floor(numElems / buckets);
|
|
1181
1259
|
let bucketsWithExtra = numElems % buckets;
|
|
1182
1260
|
let curr = lower;
|
|
1183
|
-
for (let i = 0;i < buckets; i++) {
|
|
1261
|
+
for (let i = 0; i < buckets; i++) {
|
|
1184
1262
|
let bucketSize = itemsPerBucket + (i < bucketsWithExtra ? 1 : 0);
|
|
1185
1263
|
let ourFingerprint = await this.storage.fingerprint(curr, curr + bucketSize);
|
|
1186
1264
|
curr += bucketSize;
|
|
@@ -1190,10 +1268,8 @@ class Negentropy {
|
|
|
1190
1268
|
} else {
|
|
1191
1269
|
let prevItem, currItem;
|
|
1192
1270
|
this.storage.iterate(curr - 1, curr + 1, (item, index) => {
|
|
1193
|
-
if (index === curr - 1)
|
|
1194
|
-
|
|
1195
|
-
else
|
|
1196
|
-
currItem = item;
|
|
1271
|
+
if (index === curr - 1) prevItem = item;
|
|
1272
|
+
else currItem = item;
|
|
1197
1273
|
return true;
|
|
1198
1274
|
});
|
|
1199
1275
|
nextBound = this.getMinimalBound(prevItem, currItem);
|
|
@@ -1206,13 +1282,13 @@ class Negentropy {
|
|
|
1206
1282
|
}
|
|
1207
1283
|
_renderOutput(o) {
|
|
1208
1284
|
o = o.unwrap();
|
|
1209
|
-
if (!this.wantUint8ArrayOutput)
|
|
1210
|
-
o = uint8ArrayToHex(o);
|
|
1285
|
+
if (!this.wantUint8ArrayOutput) o = uint8ArrayToHex(o);
|
|
1211
1286
|
return o;
|
|
1212
1287
|
}
|
|
1213
1288
|
exceededFrameSizeLimit(n) {
|
|
1214
1289
|
return this.frameSizeLimit && n > this.frameSizeLimit - 200;
|
|
1215
1290
|
}
|
|
1291
|
+
// Decoding
|
|
1216
1292
|
decodeTimestampIn(encoded) {
|
|
1217
1293
|
let timestamp = decodeVarInt(encoded);
|
|
1218
1294
|
timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1;
|
|
@@ -1227,11 +1303,11 @@ class Negentropy {
|
|
|
1227
1303
|
decodeBound(encoded) {
|
|
1228
1304
|
let timestamp = this.decodeTimestampIn(encoded);
|
|
1229
1305
|
let len = decodeVarInt(encoded);
|
|
1230
|
-
if (len > ID_SIZE)
|
|
1231
|
-
throw Error("bound key too long");
|
|
1306
|
+
if (len > ID_SIZE) throw Error("bound key too long");
|
|
1232
1307
|
let id = getBytes(encoded, len);
|
|
1233
1308
|
return { timestamp, id };
|
|
1234
1309
|
}
|
|
1310
|
+
// Encoding
|
|
1235
1311
|
encodeTimestampOut(timestamp) {
|
|
1236
1312
|
if (timestamp === Number.MAX_VALUE) {
|
|
1237
1313
|
this.lastTimestampOut = Number.MAX_VALUE;
|
|
@@ -1243,7 +1319,7 @@ class Negentropy {
|
|
|
1243
1319
|
return encodeVarInt(timestamp + 1);
|
|
1244
1320
|
}
|
|
1245
1321
|
encodeBound(key) {
|
|
1246
|
-
let output = new WrappedBuffer;
|
|
1322
|
+
let output = new WrappedBuffer();
|
|
1247
1323
|
output.extend(this.encodeTimestampOut(key.timestamp));
|
|
1248
1324
|
output.extend(encodeVarInt(key.id.length));
|
|
1249
1325
|
output.extend(key.id);
|
|
@@ -1256,30 +1332,24 @@ class Negentropy {
|
|
|
1256
1332
|
let sharedPrefixBytes = 0;
|
|
1257
1333
|
let currKey = curr.id;
|
|
1258
1334
|
let prevKey = prev.id;
|
|
1259
|
-
for (let i = 0;i < ID_SIZE; i++) {
|
|
1260
|
-
if (currKey[i] !== prevKey[i])
|
|
1261
|
-
break;
|
|
1335
|
+
for (let i = 0; i < ID_SIZE; i++) {
|
|
1336
|
+
if (currKey[i] !== prevKey[i]) break;
|
|
1262
1337
|
sharedPrefixBytes++;
|
|
1263
1338
|
}
|
|
1264
1339
|
return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1));
|
|
1265
1340
|
}
|
|
1266
1341
|
}
|
|
1267
|
-
}
|
|
1342
|
+
};
|
|
1268
1343
|
function loadInputBuffer(inp) {
|
|
1269
|
-
if (typeof inp === "string")
|
|
1270
|
-
|
|
1271
|
-
else if (__proto__ !== Uint8Array.prototype)
|
|
1272
|
-
inp = new Uint8Array(inp);
|
|
1344
|
+
if (typeof inp === "string") inp = hexToUint8Array(inp);
|
|
1345
|
+
else if (__proto__ !== Uint8Array.prototype) inp = new Uint8Array(inp);
|
|
1273
1346
|
return inp;
|
|
1274
1347
|
}
|
|
1275
1348
|
function hexToUint8Array(h) {
|
|
1276
|
-
if (h.startsWith("0x"))
|
|
1277
|
-
|
|
1278
|
-
if (h.length % 2 === 1)
|
|
1279
|
-
throw Error("odd length of hex string");
|
|
1349
|
+
if (h.startsWith("0x")) h = h.substr(2);
|
|
1350
|
+
if (h.length % 2 === 1) throw Error("odd length of hex string");
|
|
1280
1351
|
let arr = new Uint8Array(h.length / 2);
|
|
1281
|
-
for (let i = 0;i < arr.length; i++)
|
|
1282
|
-
arr[i] = parseInt(h.substr(i * 2, 2), 16);
|
|
1352
|
+
for (let i = 0; i < arr.length; i++) arr[i] = parseInt(h.substr(i * 2, 2), 16);
|
|
1283
1353
|
return arr;
|
|
1284
1354
|
}
|
|
1285
1355
|
var uint8ArrayToHexLookupTable = new Array(256);
|
|
@@ -1302,28 +1372,24 @@ var uint8ArrayToHexLookupTable = new Array(256);
|
|
|
1302
1372
|
"e",
|
|
1303
1373
|
"f"
|
|
1304
1374
|
];
|
|
1305
|
-
for (let i = 0;i < 256; i++) {
|
|
1375
|
+
for (let i = 0; i < 256; i++) {
|
|
1306
1376
|
uint8ArrayToHexLookupTable[i] = hexAlphabet[i >>> 4 & 15] + hexAlphabet[i & 15];
|
|
1307
1377
|
}
|
|
1308
1378
|
}
|
|
1309
1379
|
function uint8ArrayToHex(arr) {
|
|
1310
1380
|
let out = "";
|
|
1311
|
-
for (let i = 0, edx = arr.length;i < edx; i++) {
|
|
1381
|
+
for (let i = 0, edx = arr.length; i < edx; i++) {
|
|
1312
1382
|
out += uint8ArrayToHexLookupTable[arr[i]];
|
|
1313
1383
|
}
|
|
1314
1384
|
return out;
|
|
1315
1385
|
}
|
|
1316
1386
|
function compareUint8Array(a, b) {
|
|
1317
|
-
for (let i = 0;i < a.byteLength; i++) {
|
|
1318
|
-
if (a[i] < b[i])
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
if (a.byteLength > b.byteLength)
|
|
1324
|
-
return 1;
|
|
1325
|
-
if (a.byteLength < b.byteLength)
|
|
1326
|
-
return -1;
|
|
1387
|
+
for (let i = 0; i < a.byteLength; i++) {
|
|
1388
|
+
if (a[i] < b[i]) return -1;
|
|
1389
|
+
if (a[i] > b[i]) return 1;
|
|
1390
|
+
}
|
|
1391
|
+
if (a.byteLength > b.byteLength) return 1;
|
|
1392
|
+
if (a.byteLength < b.byteLength) return -1;
|
|
1327
1393
|
return 0;
|
|
1328
1394
|
}
|
|
1329
1395
|
function itemCompare(a, b) {
|
|
@@ -1339,7 +1405,7 @@ import { GiftWrap } from "nostr-tools/kinds";
|
|
|
1339
1405
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1340
1406
|
return Effect7.gen(function* () {
|
|
1341
1407
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1342
|
-
const storageVector = new NegentropyStorageVector;
|
|
1408
|
+
const storageVector = new NegentropyStorageVector();
|
|
1343
1409
|
for (const gw of allGiftWraps) {
|
|
1344
1410
|
storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
|
|
1345
1411
|
}
|
|
@@ -1352,7 +1418,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1352
1418
|
const allHaveIds = [];
|
|
1353
1419
|
const allNeedIds = [];
|
|
1354
1420
|
const subId = `neg-${Date.now()}`;
|
|
1355
|
-
const initialMsg = yield* Effect7.
|
|
1421
|
+
const initialMsg = yield* Effect7.tryPromise({
|
|
1356
1422
|
try: () => neg.initiate(),
|
|
1357
1423
|
catch: (e) => new SyncError({
|
|
1358
1424
|
message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1363,9 +1429,8 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1363
1429
|
let currentMsg = initialMsg;
|
|
1364
1430
|
while (currentMsg !== null) {
|
|
1365
1431
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1366
|
-
if (response.msgHex === null)
|
|
1367
|
-
|
|
1368
|
-
const reconcileResult = yield* Effect7.try({
|
|
1432
|
+
if (response.msgHex === null) break;
|
|
1433
|
+
const reconcileResult = yield* Effect7.tryPromise({
|
|
1369
1434
|
try: () => neg.reconcile(response.msgHex),
|
|
1370
1435
|
catch: (e) => new SyncError({
|
|
1371
1436
|
message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1374,10 +1439,8 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1374
1439
|
})
|
|
1375
1440
|
});
|
|
1376
1441
|
const [nextMsg, haveIds, needIds] = reconcileResult;
|
|
1377
|
-
for (const id of haveIds)
|
|
1378
|
-
|
|
1379
|
-
for (const id of needIds)
|
|
1380
|
-
allNeedIds.push(id);
|
|
1442
|
+
for (const id of haveIds) allHaveIds.push(id);
|
|
1443
|
+
for (const id of needIds) allNeedIds.push(id);
|
|
1381
1444
|
currentMsg = nextMsg;
|
|
1382
1445
|
}
|
|
1383
1446
|
yield* Effect7.logDebug("Negentropy reconciliation complete", {
|
|
@@ -1425,12 +1488,11 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1425
1488
|
kind: 1,
|
|
1426
1489
|
content: JSON.stringify(rotationData),
|
|
1427
1490
|
tags: [["d", `_system:rotation:${epochId}`]],
|
|
1428
|
-
created_at: Math.floor(Date.now() /
|
|
1491
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1429
1492
|
};
|
|
1430
1493
|
const wrappedEvents = [];
|
|
1431
1494
|
for (const memberPubkey of remainingMemberPubkeys) {
|
|
1432
|
-
if (memberPubkey === senderPublicKey)
|
|
1433
|
-
continue;
|
|
1495
|
+
if (memberPubkey === senderPublicKey) continue;
|
|
1434
1496
|
const wrapped = wrapEvent(rumor, senderPrivateKey, memberPubkey);
|
|
1435
1497
|
wrappedEvents.push(wrapped);
|
|
1436
1498
|
}
|
|
@@ -1443,7 +1505,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1443
1505
|
kind: 1,
|
|
1444
1506
|
content: JSON.stringify(removalData),
|
|
1445
1507
|
tags: [["d", `_system:removed:${epochId}`]],
|
|
1446
|
-
created_at: Math.floor(Date.now() /
|
|
1508
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1447
1509
|
};
|
|
1448
1510
|
const removalNotices = [];
|
|
1449
1511
|
for (const removedPubkey of removedMemberPubkeys) {
|
|
@@ -1453,8 +1515,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1453
1515
|
return { epoch, wrappedEvents, removalNotices };
|
|
1454
1516
|
}
|
|
1455
1517
|
function parseRotationEvent(content, dTag) {
|
|
1456
|
-
if (!dTag.startsWith("_system:rotation:"))
|
|
1457
|
-
return Option4.none();
|
|
1518
|
+
if (!dTag.startsWith("_system:rotation:")) return Option4.none();
|
|
1458
1519
|
try {
|
|
1459
1520
|
return Option4.some(decodeRotationData(content));
|
|
1460
1521
|
} catch {
|
|
@@ -1462,8 +1523,7 @@ function parseRotationEvent(content, dTag) {
|
|
|
1462
1523
|
}
|
|
1463
1524
|
}
|
|
1464
1525
|
function parseRemovalNotice(content, dTag) {
|
|
1465
|
-
if (!dTag.startsWith("_system:removed:"))
|
|
1466
|
-
return Option4.none();
|
|
1526
|
+
if (!dTag.startsWith("_system:removed:")) return Option4.none();
|
|
1467
1527
|
try {
|
|
1468
1528
|
return Option4.some(decodeRemovalNotice(content));
|
|
1469
1529
|
} catch {
|
|
@@ -1483,99 +1543,124 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1483
1543
|
kind: "create"
|
|
1484
1544
|
});
|
|
1485
1545
|
const forkHandled = (effect) => {
|
|
1486
|
-
Effect8.runFork(
|
|
1546
|
+
Effect8.runFork(
|
|
1547
|
+
effect.pipe(
|
|
1548
|
+
Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))),
|
|
1549
|
+
Effect8.ignore,
|
|
1550
|
+
Effect8.provide(logLayer),
|
|
1551
|
+
Effect8.forkIn(scope)
|
|
1552
|
+
)
|
|
1553
|
+
);
|
|
1487
1554
|
};
|
|
1488
1555
|
let autoFlushActive = false;
|
|
1489
1556
|
const autoFlushEffect = Effect8.gen(function* () {
|
|
1490
1557
|
const size = yield* publishQueue.size();
|
|
1491
|
-
if (size === 0)
|
|
1492
|
-
return;
|
|
1558
|
+
if (size === 0) return;
|
|
1493
1559
|
yield* syncStatus.set("syncing");
|
|
1494
1560
|
yield* publishQueue.flush(relayUrls);
|
|
1495
1561
|
const remaining = yield* publishQueue.size();
|
|
1496
|
-
if (remaining > 0)
|
|
1497
|
-
|
|
1498
|
-
|
|
1562
|
+
if (remaining > 0) yield* Effect8.fail("pending");
|
|
1563
|
+
}).pipe(
|
|
1564
|
+
Effect8.ensuring(syncStatus.set("idle")),
|
|
1565
|
+
Effect8.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
|
|
1566
|
+
Effect8.ignore
|
|
1567
|
+
);
|
|
1499
1568
|
const scheduleAutoFlush = () => {
|
|
1500
|
-
if (autoFlushActive)
|
|
1501
|
-
return;
|
|
1569
|
+
if (autoFlushActive) return;
|
|
1502
1570
|
autoFlushActive = true;
|
|
1503
|
-
forkHandled(
|
|
1504
|
-
|
|
1505
|
-
|
|
1571
|
+
forkHandled(
|
|
1572
|
+
autoFlushEffect.pipe(
|
|
1573
|
+
Effect8.ensuring(
|
|
1574
|
+
Effect8.sync(() => {
|
|
1575
|
+
autoFlushActive = false;
|
|
1576
|
+
})
|
|
1577
|
+
)
|
|
1578
|
+
)
|
|
1579
|
+
);
|
|
1506
1580
|
};
|
|
1507
1581
|
const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
|
|
1508
1582
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1509
|
-
if (!memberRecord)
|
|
1510
|
-
return false;
|
|
1583
|
+
if (!memberRecord) return false;
|
|
1511
1584
|
return !!memberRecord.removedAt;
|
|
1512
1585
|
});
|
|
1513
|
-
const
|
|
1586
|
+
const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
1587
|
+
const unwrapGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1514
1588
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1515
|
-
if (existing)
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1589
|
+
if (existing) return null;
|
|
1590
|
+
const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
|
|
1591
|
+
Effect8.orElseSucceed(() => null)
|
|
1592
|
+
);
|
|
1593
|
+
if (!rumor) {
|
|
1594
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1520
1595
|
return null;
|
|
1521
1596
|
}
|
|
1522
|
-
const rumor = unwrapResult.success;
|
|
1523
1597
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1524
1598
|
if (!dTag) {
|
|
1525
|
-
yield*
|
|
1599
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1526
1600
|
return null;
|
|
1527
1601
|
}
|
|
1528
1602
|
const colonIdx = dTag.indexOf(":");
|
|
1529
1603
|
if (colonIdx === -1) {
|
|
1530
|
-
yield*
|
|
1604
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1605
|
+
return null;
|
|
1606
|
+
}
|
|
1607
|
+
const collection2 = dTag.substring(0, colonIdx);
|
|
1608
|
+
if (!knownCollections.has(collection2)) {
|
|
1609
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1531
1610
|
return null;
|
|
1532
1611
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1612
|
+
return {
|
|
1613
|
+
giftWrap: remoteGw,
|
|
1614
|
+
rumor,
|
|
1615
|
+
collection: collection2,
|
|
1616
|
+
recordId: dTag.substring(colonIdx + 1)
|
|
1617
|
+
};
|
|
1618
|
+
});
|
|
1619
|
+
const applyUnwrappedEvent = (uw) => Effect8.gen(function* () {
|
|
1620
|
+
const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
|
|
1535
1621
|
const retention = knownCollections.get(collectionName);
|
|
1536
|
-
if (retention ===
|
|
1537
|
-
yield*
|
|
1622
|
+
if (retention === void 0) {
|
|
1623
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1538
1624
|
return null;
|
|
1539
1625
|
}
|
|
1540
1626
|
if (rumor.pubkey) {
|
|
1541
1627
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1542
1628
|
if (reject) {
|
|
1543
|
-
yield* Effect8.logWarning("Rejected write from removed member", {
|
|
1544
|
-
|
|
1629
|
+
yield* Effect8.logWarning("Rejected write from removed member", {
|
|
1630
|
+
author: rumor.pubkey.slice(0, 12)
|
|
1631
|
+
});
|
|
1632
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1545
1633
|
return null;
|
|
1546
1634
|
}
|
|
1547
1635
|
}
|
|
1548
|
-
let data = null;
|
|
1549
|
-
let kind = "u";
|
|
1550
1636
|
const parsed = yield* Effect8.try({
|
|
1551
1637
|
try: () => JSON.parse(rumor.content),
|
|
1552
|
-
catch: () =>
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
return;
|
|
1557
|
-
}));
|
|
1558
|
-
if (parsed === undefined) {
|
|
1559
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1638
|
+
catch: () => void 0
|
|
1639
|
+
}).pipe(Effect8.orElseSucceed(() => void 0));
|
|
1640
|
+
if (parsed === void 0) {
|
|
1641
|
+
yield* storeGiftWrapShell(remoteGw);
|
|
1560
1642
|
return null;
|
|
1561
1643
|
}
|
|
1644
|
+
let data = null;
|
|
1645
|
+
let kind = "u";
|
|
1562
1646
|
if (parsed === null || parsed._deleted) {
|
|
1563
1647
|
kind = "d";
|
|
1564
1648
|
} else {
|
|
1565
1649
|
data = parsed;
|
|
1566
1650
|
}
|
|
1567
|
-
const author = rumor.pubkey ||
|
|
1651
|
+
const author = rumor.pubkey || void 0;
|
|
1568
1652
|
const event = {
|
|
1569
1653
|
id: rumor.id,
|
|
1570
1654
|
collection: collectionName,
|
|
1571
1655
|
recordId,
|
|
1572
1656
|
kind,
|
|
1573
1657
|
data,
|
|
1574
|
-
createdAt: rumor.created_at *
|
|
1658
|
+
createdAt: rumor.created_at * 1e3,
|
|
1575
1659
|
author
|
|
1576
1660
|
};
|
|
1577
1661
|
yield* storage.putGiftWrap({
|
|
1578
1662
|
id: remoteGw.id,
|
|
1663
|
+
event: remoteGw,
|
|
1579
1664
|
createdAt: remoteGw.created_at
|
|
1580
1665
|
});
|
|
1581
1666
|
yield* storage.putEvent(event);
|
|
@@ -1583,12 +1668,89 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1583
1668
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1584
1669
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1585
1670
|
}
|
|
1586
|
-
yield* Effect8.logDebug("Processed gift wrap", {
|
|
1671
|
+
yield* Effect8.logDebug("Processed gift wrap", {
|
|
1672
|
+
collection: collectionName,
|
|
1673
|
+
recordId,
|
|
1674
|
+
kind,
|
|
1675
|
+
author: author?.slice(0, 12)
|
|
1676
|
+
});
|
|
1587
1677
|
if (author && onNewAuthor) {
|
|
1588
1678
|
onNewAuthor(author);
|
|
1589
1679
|
}
|
|
1590
1680
|
return collectionName;
|
|
1591
1681
|
});
|
|
1682
|
+
const reconcileRelay = (url, pubKeys) => Effect8.gen(function* () {
|
|
1683
|
+
yield* Effect8.logDebug("Syncing relay", { relay: url });
|
|
1684
|
+
const { haveIds, needIds } = yield* reconcileWithRelay(
|
|
1685
|
+
storage,
|
|
1686
|
+
relay,
|
|
1687
|
+
url,
|
|
1688
|
+
Array.from(pubKeys)
|
|
1689
|
+
).pipe(
|
|
1690
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1691
|
+
Effect8.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
|
|
1692
|
+
);
|
|
1693
|
+
yield* Effect8.logDebug("Relay reconciliation result", {
|
|
1694
|
+
relay: url,
|
|
1695
|
+
need: needIds.length,
|
|
1696
|
+
have: haveIds.length
|
|
1697
|
+
});
|
|
1698
|
+
const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
|
|
1699
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1700
|
+
Effect8.orElseSucceed(() => [])
|
|
1701
|
+
) : [];
|
|
1702
|
+
return {
|
|
1703
|
+
events,
|
|
1704
|
+
haveIds: haveIds.map((id) => ({ id, url }))
|
|
1705
|
+
};
|
|
1706
|
+
}).pipe(Effect8.withLogSpan("tablinum.reconcileRelay"));
|
|
1707
|
+
const syncAllRelays = (pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1708
|
+
const results = yield* Effect8.forEach(
|
|
1709
|
+
relayUrls,
|
|
1710
|
+
(url) => reconcileRelay(url, pubKeys),
|
|
1711
|
+
{ concurrency: "unbounded" }
|
|
1712
|
+
);
|
|
1713
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1714
|
+
const allGiftWraps = [];
|
|
1715
|
+
for (const { events } of results) {
|
|
1716
|
+
for (const ev of events) {
|
|
1717
|
+
if (!seen.has(ev.id)) {
|
|
1718
|
+
seen.add(ev.id);
|
|
1719
|
+
allGiftWraps.push(ev);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
const unwrapped = [];
|
|
1724
|
+
for (const gw of allGiftWraps) {
|
|
1725
|
+
const result = yield* unwrapGiftWrap(gw).pipe(Effect8.orElseSucceed(() => null));
|
|
1726
|
+
if (result) unwrapped.push(result);
|
|
1727
|
+
}
|
|
1728
|
+
unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
|
|
1729
|
+
for (const event of unwrapped) {
|
|
1730
|
+
const collection2 = yield* applyUnwrappedEvent(event).pipe(
|
|
1731
|
+
Effect8.orElseSucceed(() => null)
|
|
1732
|
+
);
|
|
1733
|
+
if (collection2) changedCollections.add(collection2);
|
|
1734
|
+
}
|
|
1735
|
+
const allHaveIds = results.flatMap((r) => r.haveIds);
|
|
1736
|
+
yield* Effect8.forEach(
|
|
1737
|
+
allHaveIds,
|
|
1738
|
+
({ id, url }) => Effect8.gen(function* () {
|
|
1739
|
+
const gw = yield* storage.getGiftWrap(id);
|
|
1740
|
+
if (!gw?.event) return;
|
|
1741
|
+
yield* relay.publish(gw.event, [url]).pipe(
|
|
1742
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1743
|
+
Effect8.ignore
|
|
1744
|
+
);
|
|
1745
|
+
}),
|
|
1746
|
+
{ concurrency: "unbounded", discard: true }
|
|
1747
|
+
);
|
|
1748
|
+
}).pipe(Effect8.withLogSpan("tablinum.syncAllRelays"));
|
|
1749
|
+
const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1750
|
+
const uw = yield* unwrapGiftWrap(remoteGw);
|
|
1751
|
+
if (!uw) return null;
|
|
1752
|
+
return yield* applyUnwrappedEvent(uw);
|
|
1753
|
+
});
|
|
1592
1754
|
const processRealtimeGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1593
1755
|
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
|
|
1594
1756
|
if (collection2) {
|
|
@@ -1596,32 +1758,34 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1596
1758
|
}
|
|
1597
1759
|
});
|
|
1598
1760
|
const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1599
|
-
const unwrapResult = yield* Effect8.result(
|
|
1600
|
-
try
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1761
|
+
const unwrapResult = yield* Effect8.result(
|
|
1762
|
+
Effect8.try({
|
|
1763
|
+
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1764
|
+
catch: (e) => new CryptoError({
|
|
1765
|
+
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
1766
|
+
cause: e
|
|
1767
|
+
})
|
|
1604
1768
|
})
|
|
1605
|
-
|
|
1606
|
-
if (unwrapResult._tag === "Failure")
|
|
1607
|
-
return false;
|
|
1769
|
+
);
|
|
1770
|
+
if (unwrapResult._tag === "Failure") return false;
|
|
1608
1771
|
const rumor = unwrapResult.success;
|
|
1609
1772
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1610
|
-
if (!dTag)
|
|
1611
|
-
return false;
|
|
1773
|
+
if (!dTag) return false;
|
|
1612
1774
|
const removalNoticeOpt = parseRemovalNotice(rumor.content, dTag);
|
|
1613
1775
|
if (Option5.isSome(removalNoticeOpt)) {
|
|
1614
|
-
if (onRemoved)
|
|
1615
|
-
onRemoved(removalNoticeOpt.value);
|
|
1776
|
+
if (onRemoved) onRemoved(removalNoticeOpt.value);
|
|
1616
1777
|
return true;
|
|
1617
1778
|
}
|
|
1618
1779
|
const rotationDataOpt = parseRotationEvent(rumor.content, dTag);
|
|
1619
|
-
if (Option5.isNone(rotationDataOpt))
|
|
1620
|
-
return false;
|
|
1780
|
+
if (Option5.isNone(rotationDataOpt)) return false;
|
|
1621
1781
|
const rotationData = rotationDataOpt.value;
|
|
1622
|
-
if (epochStore.epochs.has(rotationData.epochId))
|
|
1623
|
-
|
|
1624
|
-
|
|
1782
|
+
if (epochStore.epochs.has(rotationData.epochId)) return false;
|
|
1783
|
+
const epoch = createEpochKey(
|
|
1784
|
+
rotationData.epochId,
|
|
1785
|
+
rotationData.epochKey,
|
|
1786
|
+
rumor.pubkey || "",
|
|
1787
|
+
rotationData.parentEpoch
|
|
1788
|
+
);
|
|
1625
1789
|
addEpoch(epochStore, epoch);
|
|
1626
1790
|
epochStore.currentEpochId = epoch.id;
|
|
1627
1791
|
let membersChanged = false;
|
|
@@ -1641,79 +1805,107 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1641
1805
|
membersChanged = true;
|
|
1642
1806
|
}
|
|
1643
1807
|
}
|
|
1644
|
-
if (membersChanged && onMembersChanged)
|
|
1645
|
-
onMembersChanged();
|
|
1808
|
+
if (membersChanged && onMembersChanged) onMembersChanged();
|
|
1646
1809
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1647
1810
|
return true;
|
|
1648
1811
|
});
|
|
1649
|
-
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
if (
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
return;
|
|
1677
|
-
yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1678
|
-
}), { discard: true });
|
|
1679
|
-
}
|
|
1680
|
-
}).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
|
|
1812
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1813
|
+
relayUrls,
|
|
1814
|
+
(url) => Effect8.gen(function* () {
|
|
1815
|
+
yield* relay.subscribe(filter, url, (event) => {
|
|
1816
|
+
forkHandled(onEvent(event));
|
|
1817
|
+
}).pipe(
|
|
1818
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1819
|
+
Effect8.ignore
|
|
1820
|
+
);
|
|
1821
|
+
}),
|
|
1822
|
+
{ concurrency: "unbounded", discard: true }
|
|
1823
|
+
);
|
|
1824
|
+
let healingActive = false;
|
|
1825
|
+
const healingEffect = Effect8.gen(function* () {
|
|
1826
|
+
if (!healingActive) return;
|
|
1827
|
+
const status = yield* syncStatus.get();
|
|
1828
|
+
if (status === "syncing") return;
|
|
1829
|
+
yield* syncStatus.set("syncing");
|
|
1830
|
+
yield* Effect8.gen(function* () {
|
|
1831
|
+
const pubKeys = getSubscriptionPubKeys();
|
|
1832
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1833
|
+
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1834
|
+
if (changedCollections.size > 0) {
|
|
1835
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1836
|
+
}
|
|
1837
|
+
}).pipe(Effect8.ensuring(syncStatus.set("idle")));
|
|
1838
|
+
}).pipe(Effect8.ignore);
|
|
1681
1839
|
const handle = {
|
|
1682
1840
|
sync: () => Effect8.gen(function* () {
|
|
1683
1841
|
yield* Effect8.logInfo("Sync started");
|
|
1684
1842
|
yield* syncStatus.set("syncing");
|
|
1685
1843
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1686
|
-
const changedCollections = new Set;
|
|
1844
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1687
1845
|
yield* Effect8.gen(function* () {
|
|
1688
1846
|
const pubKeys = getSubscriptionPubKeys();
|
|
1689
|
-
yield*
|
|
1690
|
-
discard: true
|
|
1691
|
-
});
|
|
1847
|
+
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1692
1848
|
yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
|
|
1693
|
-
}).pipe(
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1849
|
+
}).pipe(
|
|
1850
|
+
Effect8.ensuring(
|
|
1851
|
+
Effect8.gen(function* () {
|
|
1852
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1853
|
+
yield* syncStatus.set("idle");
|
|
1854
|
+
})
|
|
1855
|
+
)
|
|
1856
|
+
);
|
|
1697
1857
|
yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
1698
1858
|
}).pipe(Effect8.withLogSpan("tablinum.sync")),
|
|
1699
1859
|
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1700
|
-
if (!giftWrap.event)
|
|
1701
|
-
|
|
1702
|
-
|
|
1860
|
+
if (!giftWrap.event) return;
|
|
1861
|
+
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1862
|
+
Effect8.tapError(
|
|
1863
|
+
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1864
|
+
Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
1865
|
+
Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
|
|
1866
|
+
)
|
|
1867
|
+
),
|
|
1868
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1869
|
+
Effect8.ignore
|
|
1870
|
+
);
|
|
1703
1871
|
}),
|
|
1704
1872
|
startSubscription: () => Effect8.gen(function* () {
|
|
1705
1873
|
const pubKeys = getSubscriptionPubKeys();
|
|
1706
1874
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1707
1875
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1708
|
-
yield* subscribeAcrossRelays(
|
|
1876
|
+
yield* subscribeAcrossRelays(
|
|
1877
|
+
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1878
|
+
(event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
|
|
1879
|
+
);
|
|
1709
1880
|
}
|
|
1710
1881
|
}),
|
|
1711
|
-
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap)
|
|
1882
|
+
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap),
|
|
1883
|
+
startHealing: () => {
|
|
1884
|
+
if (healingActive) return;
|
|
1885
|
+
healingActive = true;
|
|
1886
|
+
forkHandled(
|
|
1887
|
+
Effect8.sleep(Duration.minutes(5)).pipe(
|
|
1888
|
+
Effect8.andThen(healingEffect),
|
|
1889
|
+
Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
1890
|
+
Effect8.ensuring(Effect8.sync(() => {
|
|
1891
|
+
healingActive = false;
|
|
1892
|
+
}))
|
|
1893
|
+
)
|
|
1894
|
+
);
|
|
1895
|
+
},
|
|
1896
|
+
stopHealing: () => {
|
|
1897
|
+
healingActive = false;
|
|
1898
|
+
}
|
|
1712
1899
|
};
|
|
1713
|
-
forkHandled(
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1900
|
+
forkHandled(
|
|
1901
|
+
publishQueue.size().pipe(
|
|
1902
|
+
Effect8.flatMap(
|
|
1903
|
+
(size) => Effect8.sync(() => {
|
|
1904
|
+
if (size > 0) scheduleAutoFlush();
|
|
1905
|
+
})
|
|
1906
|
+
)
|
|
1907
|
+
)
|
|
1908
|
+
);
|
|
1717
1909
|
return handle;
|
|
1718
1910
|
}
|
|
1719
1911
|
|
|
@@ -1769,9 +1961,14 @@ var decodeAuthorProfile = Schema5.decodeUnknownEffect(Schema5.fromJsonString(Aut
|
|
|
1769
1961
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1770
1962
|
return Effect9.gen(function* () {
|
|
1771
1963
|
for (const url of relayUrls) {
|
|
1772
|
-
const result = yield* Effect9.result(
|
|
1964
|
+
const result = yield* Effect9.result(
|
|
1965
|
+
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
1966
|
+
);
|
|
1773
1967
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
1774
|
-
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1968
|
+
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1969
|
+
Effect9.map(Option6.some),
|
|
1970
|
+
Effect9.orElseSucceed(() => Option6.none())
|
|
1971
|
+
);
|
|
1775
1972
|
}
|
|
1776
1973
|
}
|
|
1777
1974
|
return Option6.none();
|
|
@@ -1780,45 +1977,44 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
|
1780
1977
|
|
|
1781
1978
|
// src/services/Identity.ts
|
|
1782
1979
|
import { ServiceMap as ServiceMap3 } from "effect";
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
}
|
|
1980
|
+
var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
|
|
1981
|
+
};
|
|
1786
1982
|
|
|
1787
1983
|
// src/services/EpochStore.ts
|
|
1788
1984
|
import { ServiceMap as ServiceMap4 } from "effect";
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1985
|
+
var EpochStore = class extends ServiceMap4.Service()(
|
|
1986
|
+
"tablinum/EpochStore"
|
|
1987
|
+
) {
|
|
1988
|
+
};
|
|
1792
1989
|
|
|
1793
1990
|
// src/services/Storage.ts
|
|
1794
1991
|
import { ServiceMap as ServiceMap5 } from "effect";
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
}
|
|
1992
|
+
var Storage = class extends ServiceMap5.Service()("tablinum/Storage") {
|
|
1993
|
+
};
|
|
1798
1994
|
|
|
1799
1995
|
// src/services/Relay.ts
|
|
1800
1996
|
import { ServiceMap as ServiceMap6 } from "effect";
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
}
|
|
1997
|
+
var Relay = class extends ServiceMap6.Service()("tablinum/Relay") {
|
|
1998
|
+
};
|
|
1804
1999
|
|
|
1805
2000
|
// src/services/GiftWrap.ts
|
|
1806
2001
|
import { ServiceMap as ServiceMap7 } from "effect";
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
}
|
|
2002
|
+
var GiftWrap3 = class extends ServiceMap7.Service()("tablinum/GiftWrap") {
|
|
2003
|
+
};
|
|
1810
2004
|
|
|
1811
2005
|
// src/services/PublishQueue.ts
|
|
1812
2006
|
import { ServiceMap as ServiceMap8 } from "effect";
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2007
|
+
var PublishQueue = class extends ServiceMap8.Service()(
|
|
2008
|
+
"tablinum/PublishQueue"
|
|
2009
|
+
) {
|
|
2010
|
+
};
|
|
1816
2011
|
|
|
1817
2012
|
// src/services/SyncStatus.ts
|
|
1818
2013
|
import { ServiceMap as ServiceMap9 } from "effect";
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2014
|
+
var SyncStatus = class extends ServiceMap9.Service()(
|
|
2015
|
+
"tablinum/SyncStatus"
|
|
2016
|
+
) {
|
|
2017
|
+
};
|
|
1822
2018
|
|
|
1823
2019
|
// src/layers/IdentityLive.ts
|
|
1824
2020
|
import { Effect as Effect11, Layer as Layer2 } from "effect";
|
|
@@ -1859,47 +2055,59 @@ function createIdentity(suppliedKey) {
|
|
|
1859
2055
|
}
|
|
1860
2056
|
|
|
1861
2057
|
// src/layers/IdentityLive.ts
|
|
1862
|
-
var IdentityLive = Layer2.effect(
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
})
|
|
2058
|
+
var IdentityLive = Layer2.effect(
|
|
2059
|
+
Identity,
|
|
2060
|
+
Effect11.gen(function* () {
|
|
2061
|
+
const config = yield* Config;
|
|
2062
|
+
const storage = yield* Storage;
|
|
2063
|
+
const idbKey = yield* storage.getMeta("identity_key");
|
|
2064
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
|
|
2065
|
+
const identity = yield* createIdentity(resolvedKey);
|
|
2066
|
+
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2067
|
+
yield* Effect11.logInfo("Identity loaded", {
|
|
2068
|
+
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2069
|
+
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2070
|
+
});
|
|
2071
|
+
return identity;
|
|
2072
|
+
})
|
|
2073
|
+
);
|
|
1875
2074
|
|
|
1876
2075
|
// src/layers/EpochStoreLive.ts
|
|
1877
2076
|
import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
|
|
1878
2077
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1879
2078
|
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
1880
|
-
var EpochStoreLive = Layer3.effect(
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
const
|
|
1887
|
-
if (
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
}
|
|
2079
|
+
var EpochStoreLive = Layer3.effect(
|
|
2080
|
+
EpochStore,
|
|
2081
|
+
Effect12.gen(function* () {
|
|
2082
|
+
const config = yield* Config;
|
|
2083
|
+
const identity = yield* Identity;
|
|
2084
|
+
const storage = yield* Storage;
|
|
2085
|
+
const idbRaw = yield* storage.getMeta("epochs");
|
|
2086
|
+
if (typeof idbRaw === "string") {
|
|
2087
|
+
const idbStore = deserializeEpochStore(idbRaw);
|
|
2088
|
+
if (Option7.isSome(idbStore)) {
|
|
2089
|
+
yield* Effect12.logInfo("Epoch store loaded", {
|
|
2090
|
+
source: "storage",
|
|
2091
|
+
epochs: idbStore.value.epochs.size
|
|
2092
|
+
});
|
|
2093
|
+
return idbStore.value;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
2097
|
+
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2098
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2099
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2100
|
+
return store2;
|
|
2101
|
+
}
|
|
2102
|
+
const store = createEpochStoreFromInputs(
|
|
2103
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
|
|
2104
|
+
{ createdBy: identity.publicKey }
|
|
2105
|
+
);
|
|
2106
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2107
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2108
|
+
return store;
|
|
2109
|
+
})
|
|
2110
|
+
);
|
|
1903
2111
|
|
|
1904
2112
|
// src/layers/StorageLive.ts
|
|
1905
2113
|
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
@@ -1907,6 +2115,68 @@ import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
|
1907
2115
|
// src/storage/idb.ts
|
|
1908
2116
|
import { Effect as Effect13 } from "effect";
|
|
1909
2117
|
import { openDB } from "idb";
|
|
2118
|
+
|
|
2119
|
+
// src/sync/compact-event.ts
|
|
2120
|
+
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2121
|
+
var VERSION = 1;
|
|
2122
|
+
var HEADER_SIZE = 133;
|
|
2123
|
+
function base64ToBytes(base64) {
|
|
2124
|
+
const binary = atob(base64);
|
|
2125
|
+
const bytes = new Uint8Array(binary.length);
|
|
2126
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2127
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2128
|
+
}
|
|
2129
|
+
return bytes;
|
|
2130
|
+
}
|
|
2131
|
+
function bytesToBase64(bytes) {
|
|
2132
|
+
let binary = "";
|
|
2133
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2134
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2135
|
+
}
|
|
2136
|
+
return btoa(binary);
|
|
2137
|
+
}
|
|
2138
|
+
function packEvent(event) {
|
|
2139
|
+
const pubkey = hexToBytes4(event.pubkey);
|
|
2140
|
+
const sig = hexToBytes4(event.sig);
|
|
2141
|
+
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2142
|
+
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2143
|
+
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2144
|
+
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2145
|
+
}
|
|
2146
|
+
const recipient = hexToBytes4(recipientTag[1]);
|
|
2147
|
+
const createdAtBuf = new Uint8Array(4);
|
|
2148
|
+
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2149
|
+
const content = base64ToBytes(event.content);
|
|
2150
|
+
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2151
|
+
result[0] = VERSION;
|
|
2152
|
+
result.set(pubkey, 1);
|
|
2153
|
+
result.set(sig, 33);
|
|
2154
|
+
result.set(recipient, 97);
|
|
2155
|
+
result.set(createdAtBuf, 129);
|
|
2156
|
+
result.set(content, HEADER_SIZE);
|
|
2157
|
+
return result;
|
|
2158
|
+
}
|
|
2159
|
+
function unpackEvent(id, compact) {
|
|
2160
|
+
const version = compact[0];
|
|
2161
|
+
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2162
|
+
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2163
|
+
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2164
|
+
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2165
|
+
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2166
|
+
const createdAt = dv.getUint32(0, false);
|
|
2167
|
+
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2168
|
+
return {
|
|
2169
|
+
id,
|
|
2170
|
+
pubkey,
|
|
2171
|
+
sig,
|
|
2172
|
+
created_at: createdAt,
|
|
2173
|
+
kind: 1059,
|
|
2174
|
+
tags: [["p", recipient]],
|
|
2175
|
+
content
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// src/storage/idb.ts
|
|
1910
2180
|
var DB_NAME = "tablinum";
|
|
1911
2181
|
function storeName(collection2) {
|
|
1912
2182
|
return `col_${collection2}`;
|
|
@@ -1937,7 +2207,7 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1937
2207
|
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
1938
2208
|
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
1939
2209
|
}
|
|
1940
|
-
const expectedStores = new Set;
|
|
2210
|
+
const expectedStores = /* @__PURE__ */ new Set();
|
|
1941
2211
|
for (const [, def] of Object.entries(schema)) {
|
|
1942
2212
|
const sn = storeName(def.name);
|
|
1943
2213
|
expectedStores.add(sn);
|
|
@@ -1951,12 +2221,10 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1951
2221
|
const existingIndices = new Set(Array.from(store.indexNames));
|
|
1952
2222
|
const wantedIndices = new Set(def.indices ?? []);
|
|
1953
2223
|
for (const idx of existingIndices) {
|
|
1954
|
-
if (!wantedIndices.has(idx))
|
|
1955
|
-
store.deleteIndex(idx);
|
|
2224
|
+
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
1956
2225
|
}
|
|
1957
2226
|
for (const idx of wantedIndices) {
|
|
1958
|
-
if (!existingIndices.has(idx))
|
|
1959
|
-
store.createIndex(idx, idx);
|
|
2227
|
+
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
1960
2228
|
}
|
|
1961
2229
|
}
|
|
1962
2230
|
}
|
|
@@ -1971,6 +2239,13 @@ function openIDBStorage(dbName, schema) {
|
|
|
1971
2239
|
return Effect13.gen(function* () {
|
|
1972
2240
|
const name = dbName ?? DB_NAME;
|
|
1973
2241
|
const schemaSig = computeSchemaSig(schema);
|
|
2242
|
+
if (typeof indexedDB === "undefined") {
|
|
2243
|
+
return yield* Effect13.fail(
|
|
2244
|
+
new StorageError({
|
|
2245
|
+
message: "IndexedDB is not available in this environment"
|
|
2246
|
+
})
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
1974
2249
|
const probeDb = yield* Effect13.tryPromise({
|
|
1975
2250
|
try: () => openDB(name),
|
|
1976
2251
|
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
@@ -1981,7 +2256,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
1981
2256
|
const storedSig = yield* Effect13.tryPromise({
|
|
1982
2257
|
try: () => probeDb.get("_meta", "schema_sig"),
|
|
1983
2258
|
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
1984
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(
|
|
2259
|
+
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
1985
2260
|
needsUpgrade = storedSig !== schemaSig;
|
|
1986
2261
|
}
|
|
1987
2262
|
probeDb.close();
|
|
@@ -1998,9 +2273,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
1998
2273
|
});
|
|
1999
2274
|
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2000
2275
|
const handle = {
|
|
2001
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() =>
|
|
2002
|
-
return;
|
|
2003
|
-
})),
|
|
2276
|
+
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
2004
2277
|
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2005
2278
|
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2006
2279
|
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
@@ -2020,29 +2293,52 @@ function openIDBStorage(dbName, schema) {
|
|
|
2020
2293
|
}
|
|
2021
2294
|
return results;
|
|
2022
2295
|
}),
|
|
2023
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() =>
|
|
2024
|
-
return;
|
|
2025
|
-
})),
|
|
2296
|
+
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2026
2297
|
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2027
2298
|
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2028
|
-
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
const existing = await db.get("giftwraps", id);
|
|
2039
|
-
if (existing) {
|
|
2040
|
-
await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
|
|
2299
|
+
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2300
|
+
"getEventsByRecord",
|
|
2301
|
+
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2302
|
+
),
|
|
2303
|
+
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2304
|
+
if (gw.event) {
|
|
2305
|
+
const compact = packEvent(gw.event);
|
|
2306
|
+
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2307
|
+
} else {
|
|
2308
|
+
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2041
2309
|
}
|
|
2042
2310
|
}),
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2311
|
+
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2312
|
+
const raw = await db.get("giftwraps", id);
|
|
2313
|
+
if (!raw) return void 0;
|
|
2314
|
+
if (raw.compact) {
|
|
2315
|
+
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2316
|
+
}
|
|
2317
|
+
if (raw.event) {
|
|
2318
|
+
const compact = packEvent(raw.event);
|
|
2319
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2320
|
+
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2321
|
+
}
|
|
2322
|
+
return { id: raw.id, createdAt: raw.createdAt };
|
|
2323
|
+
}),
|
|
2324
|
+
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2325
|
+
const raws = await db.getAll("giftwraps");
|
|
2326
|
+
const results = [];
|
|
2327
|
+
for (const raw of raws) {
|
|
2328
|
+
if (raw.compact) {
|
|
2329
|
+
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2330
|
+
} else if (raw.event) {
|
|
2331
|
+
const compact = packEvent(raw.event);
|
|
2332
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2333
|
+
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2334
|
+
} else {
|
|
2335
|
+
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return results;
|
|
2339
|
+
}),
|
|
2340
|
+
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2341
|
+
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2046
2342
|
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2047
2343
|
const existing = await db.get("events", id);
|
|
2048
2344
|
if (existing) {
|
|
@@ -2050,9 +2346,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2050
2346
|
}
|
|
2051
2347
|
}),
|
|
2052
2348
|
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2053
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() =>
|
|
2054
|
-
return;
|
|
2055
|
-
})),
|
|
2349
|
+
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2056
2350
|
close: () => Effect13.sync(() => db.close())
|
|
2057
2351
|
};
|
|
2058
2352
|
return handle;
|
|
@@ -2060,15 +2354,18 @@ function openIDBStorage(dbName, schema) {
|
|
|
2060
2354
|
}
|
|
2061
2355
|
|
|
2062
2356
|
// src/layers/StorageLive.ts
|
|
2063
|
-
var StorageLive = Layer4.effect(
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
})
|
|
2357
|
+
var StorageLive = Layer4.effect(
|
|
2358
|
+
Storage,
|
|
2359
|
+
Effect14.gen(function* () {
|
|
2360
|
+
const config = yield* Config;
|
|
2361
|
+
const handle = yield* openIDBStorage(config.dbName, {
|
|
2362
|
+
...config.schema,
|
|
2363
|
+
_members: membersCollectionDef
|
|
2364
|
+
});
|
|
2365
|
+
yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
|
|
2366
|
+
return handle;
|
|
2367
|
+
})
|
|
2368
|
+
);
|
|
2072
2369
|
|
|
2073
2370
|
// src/layers/RelayLive.ts
|
|
2074
2371
|
import { Layer as Layer5 } from "effect";
|
|
@@ -2082,32 +2379,41 @@ var NegMessageFrameSchema = Schema6.Tuple([
|
|
|
2082
2379
|
Schema6.String
|
|
2083
2380
|
]);
|
|
2084
2381
|
var NegErrorFrameSchema = Schema6.Tuple([Schema6.Literal("NEG-ERR"), Schema6.String, Schema6.String]);
|
|
2085
|
-
var decodeNegFrame = Schema6.decodeUnknownEffect(
|
|
2382
|
+
var decodeNegFrame = Schema6.decodeUnknownEffect(
|
|
2383
|
+
Schema6.fromJsonString(Schema6.Union([NegMessageFrameSchema, NegErrorFrameSchema]))
|
|
2384
|
+
);
|
|
2086
2385
|
function parseNegMessageFrame(data) {
|
|
2087
|
-
return Effect15.runSync(
|
|
2386
|
+
return Effect15.runSync(
|
|
2387
|
+
decodeNegFrame(data).pipe(
|
|
2388
|
+
Effect15.map(Option8.some),
|
|
2389
|
+
Effect15.orElseSucceed(() => Option8.none())
|
|
2390
|
+
)
|
|
2391
|
+
);
|
|
2088
2392
|
}
|
|
2089
2393
|
function createRelayHandle() {
|
|
2090
2394
|
return Effect15.gen(function* () {
|
|
2091
2395
|
const relayScope = yield* Effect15.scope;
|
|
2092
2396
|
const connections = yield* ScopedCache.make({
|
|
2093
2397
|
capacity: 64,
|
|
2094
|
-
lookup: (url) => Effect15.acquireRelease(
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2398
|
+
lookup: (url) => Effect15.acquireRelease(
|
|
2399
|
+
Effect15.tryPromise({
|
|
2400
|
+
try: () => Relay2.connect(url),
|
|
2401
|
+
catch: (e) => new RelayError({
|
|
2402
|
+
message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2403
|
+
url,
|
|
2404
|
+
cause: e
|
|
2405
|
+
})
|
|
2406
|
+
}),
|
|
2407
|
+
(relay) => Effect15.sync(() => {
|
|
2408
|
+
relay.close();
|
|
2100
2409
|
})
|
|
2101
|
-
|
|
2102
|
-
relay.close();
|
|
2103
|
-
}))
|
|
2410
|
+
)
|
|
2104
2411
|
});
|
|
2105
|
-
const connectedUrls = new Set;
|
|
2106
|
-
const statusListeners = new Set;
|
|
2412
|
+
const connectedUrls = /* @__PURE__ */ new Set();
|
|
2413
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
2107
2414
|
const notifyStatus = () => {
|
|
2108
2415
|
const status = { connectedUrls: [...connectedUrls] };
|
|
2109
|
-
for (const listener of statusListeners)
|
|
2110
|
-
listener(status);
|
|
2416
|
+
for (const listener of statusListeners) listener(status);
|
|
2111
2417
|
};
|
|
2112
2418
|
const markConnected = (url) => {
|
|
2113
2419
|
if (!connectedUrls.has(url)) {
|
|
@@ -2121,66 +2427,98 @@ function createRelayHandle() {
|
|
|
2121
2427
|
notifyStatus();
|
|
2122
2428
|
}
|
|
2123
2429
|
};
|
|
2124
|
-
const getRelay = (url) => ScopedCache.get(connections, url).pipe(
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
sub
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2430
|
+
const getRelay = (url) => ScopedCache.get(connections, url).pipe(
|
|
2431
|
+
Effect15.flatMap(
|
|
2432
|
+
(relay) => relay.connected === false ? ScopedCache.invalidate(connections, url).pipe(
|
|
2433
|
+
Effect15.andThen(ScopedCache.get(connections, url))
|
|
2434
|
+
) : Effect15.succeed(relay)
|
|
2435
|
+
)
|
|
2436
|
+
);
|
|
2437
|
+
const withRelay = (url, run) => getRelay(url).pipe(
|
|
2438
|
+
Effect15.tap(() => Effect15.sync(() => markConnected(url))),
|
|
2439
|
+
Effect15.flatMap((relay) => run(relay)),
|
|
2440
|
+
Effect15.tapError(
|
|
2441
|
+
() => ScopedCache.invalidate(connections, url).pipe(
|
|
2442
|
+
Effect15.tap(() => Effect15.sync(() => markDisconnected(url)))
|
|
2443
|
+
)
|
|
2444
|
+
)
|
|
2445
|
+
);
|
|
2446
|
+
const collectEvents = (url, filters) => withRelay(
|
|
2447
|
+
url,
|
|
2448
|
+
(relay) => Effect15.callback((resume) => {
|
|
2449
|
+
const events = [];
|
|
2450
|
+
let settled = false;
|
|
2451
|
+
let timer;
|
|
2452
|
+
let sub;
|
|
2453
|
+
const cleanup = () => {
|
|
2454
|
+
settled = true;
|
|
2455
|
+
if (timer !== void 0) {
|
|
2456
|
+
clearTimeout(timer);
|
|
2457
|
+
timer = void 0;
|
|
2458
|
+
}
|
|
2459
|
+
sub?.close();
|
|
2460
|
+
sub = void 0;
|
|
2461
|
+
};
|
|
2462
|
+
const fail = (e) => resume(
|
|
2463
|
+
Effect15.fail(
|
|
2464
|
+
new RelayError({
|
|
2465
|
+
message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2466
|
+
url,
|
|
2467
|
+
cause: e
|
|
2468
|
+
})
|
|
2469
|
+
)
|
|
2470
|
+
);
|
|
2471
|
+
try {
|
|
2472
|
+
sub = relay.subscribe([...filters], {
|
|
2473
|
+
onevent(evt) {
|
|
2474
|
+
if (!settled) {
|
|
2475
|
+
events.push(evt);
|
|
2476
|
+
}
|
|
2477
|
+
},
|
|
2478
|
+
oneose() {
|
|
2479
|
+
if (settled) return;
|
|
2480
|
+
cleanup();
|
|
2481
|
+
resume(Effect15.succeed(events));
|
|
2150
2482
|
}
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
if (settled)
|
|
2154
|
-
return;
|
|
2483
|
+
});
|
|
2484
|
+
timer = setTimeout(() => {
|
|
2485
|
+
if (settled) return;
|
|
2155
2486
|
cleanup();
|
|
2156
2487
|
resume(Effect15.succeed(events));
|
|
2157
|
-
}
|
|
2158
|
-
})
|
|
2159
|
-
timer = setTimeout(() => {
|
|
2160
|
-
if (settled)
|
|
2161
|
-
return;
|
|
2488
|
+
}, 1e4);
|
|
2489
|
+
} catch (e) {
|
|
2162
2490
|
cleanup();
|
|
2163
|
-
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
}
|
|
2169
|
-
return Effect15.sync(cleanup);
|
|
2170
|
-
}));
|
|
2491
|
+
fail(e);
|
|
2492
|
+
}
|
|
2493
|
+
return Effect15.sync(cleanup);
|
|
2494
|
+
})
|
|
2495
|
+
);
|
|
2171
2496
|
return {
|
|
2172
2497
|
publish: (event, urls) => Effect15.gen(function* () {
|
|
2173
|
-
const results = yield* Effect15.forEach(
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2498
|
+
const results = yield* Effect15.forEach(
|
|
2499
|
+
urls,
|
|
2500
|
+
(url) => Effect15.result(
|
|
2501
|
+
withRelay(
|
|
2502
|
+
url,
|
|
2503
|
+
(relay) => Effect15.tryPromise({
|
|
2504
|
+
try: () => relay.publish(event),
|
|
2505
|
+
catch: (e) => new RelayError({
|
|
2506
|
+
message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2507
|
+
url,
|
|
2508
|
+
cause: e
|
|
2509
|
+
})
|
|
2510
|
+
}).pipe(
|
|
2511
|
+
Effect15.timeoutOrElse({
|
|
2512
|
+
duration: "10 seconds",
|
|
2513
|
+
onTimeout: () => Effect15.fail(
|
|
2514
|
+
new RelayError({ message: `Publish to ${url} timed out`, url })
|
|
2515
|
+
)
|
|
2516
|
+
})
|
|
2517
|
+
)
|
|
2518
|
+
)
|
|
2519
|
+
),
|
|
2520
|
+
{ concurrency: "unbounded" }
|
|
2521
|
+
);
|
|
2184
2522
|
const failures = results.filter((r) => r._tag === "Failure");
|
|
2185
2523
|
if (failures.length === urls.length && urls.length > 0) {
|
|
2186
2524
|
return yield* new RelayError({
|
|
@@ -2189,90 +2527,104 @@ function createRelayHandle() {
|
|
|
2189
2527
|
}
|
|
2190
2528
|
}),
|
|
2191
2529
|
fetchEvents: (ids, url) => Effect15.gen(function* () {
|
|
2192
|
-
if (ids.length === 0)
|
|
2193
|
-
return [];
|
|
2530
|
+
if (ids.length === 0) return [];
|
|
2194
2531
|
return yield* collectEvents(url, [{ ids }]);
|
|
2195
2532
|
}),
|
|
2196
2533
|
fetchByFilter: (filter, url) => collectEvents(url, [filter]),
|
|
2197
|
-
subscribe: (filter, url, onEvent) => withRelay(
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
sub
|
|
2225
|
-
ws
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
return;
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
try {
|
|
2252
|
-
sub = relay.subscribe([filter], {
|
|
2253
|
-
onevent() {},
|
|
2254
|
-
oneose() {}
|
|
2255
|
-
});
|
|
2256
|
-
ws = relay._ws || relay.ws;
|
|
2257
|
-
if (!ws) {
|
|
2534
|
+
subscribe: (filter, url, onEvent) => withRelay(
|
|
2535
|
+
url,
|
|
2536
|
+
(relay) => Effect15.acquireRelease(
|
|
2537
|
+
Effect15.try({
|
|
2538
|
+
try: () => relay.subscribe([filter], {
|
|
2539
|
+
onevent(evt) {
|
|
2540
|
+
onEvent(evt);
|
|
2541
|
+
},
|
|
2542
|
+
oneose() {
|
|
2543
|
+
}
|
|
2544
|
+
}),
|
|
2545
|
+
catch: (e) => new RelayError({
|
|
2546
|
+
message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2547
|
+
url,
|
|
2548
|
+
cause: e
|
|
2549
|
+
})
|
|
2550
|
+
}),
|
|
2551
|
+
(sub) => Effect15.sync(() => {
|
|
2552
|
+
sub.close();
|
|
2553
|
+
})
|
|
2554
|
+
).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)
|
|
2555
|
+
),
|
|
2556
|
+
sendNegMsg: (url, subId, filter, msgHex) => withRelay(
|
|
2557
|
+
url,
|
|
2558
|
+
(relay) => Effect15.callback((resume) => {
|
|
2559
|
+
let settled = false;
|
|
2560
|
+
let timer;
|
|
2561
|
+
let sub;
|
|
2562
|
+
let ws;
|
|
2563
|
+
const cleanup = () => {
|
|
2564
|
+
settled = true;
|
|
2565
|
+
if (timer !== void 0) {
|
|
2566
|
+
clearTimeout(timer);
|
|
2567
|
+
timer = void 0;
|
|
2568
|
+
}
|
|
2569
|
+
sub?.close();
|
|
2570
|
+
sub = void 0;
|
|
2571
|
+
ws?.removeEventListener("message", handler);
|
|
2572
|
+
ws = void 0;
|
|
2573
|
+
};
|
|
2574
|
+
const fail = (e) => resume(
|
|
2575
|
+
Effect15.fail(
|
|
2576
|
+
new RelayError({
|
|
2577
|
+
message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2578
|
+
url,
|
|
2579
|
+
cause: e
|
|
2580
|
+
})
|
|
2581
|
+
)
|
|
2582
|
+
);
|
|
2583
|
+
const handler = (msg) => {
|
|
2584
|
+
if (settled || typeof msg.data !== "string") return;
|
|
2585
|
+
const frameOpt = parseNegMessageFrame(msg.data);
|
|
2586
|
+
if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId) return;
|
|
2587
|
+
const frame = frameOpt.value;
|
|
2258
2588
|
cleanup();
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2589
|
+
if (frame[0] === "NEG-MSG") {
|
|
2590
|
+
resume(
|
|
2591
|
+
Effect15.succeed({
|
|
2592
|
+
msgHex: frame[2],
|
|
2593
|
+
haveIds: [],
|
|
2594
|
+
needIds: []
|
|
2595
|
+
})
|
|
2596
|
+
);
|
|
2264
2597
|
return;
|
|
2598
|
+
}
|
|
2599
|
+
fail(new Error(`NEG-ERR: ${frame[2]}`));
|
|
2600
|
+
};
|
|
2601
|
+
try {
|
|
2602
|
+
sub = relay.subscribe([filter], {
|
|
2603
|
+
onevent() {
|
|
2604
|
+
},
|
|
2605
|
+
oneose() {
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
ws = relay._ws || relay.ws;
|
|
2609
|
+
if (!ws) {
|
|
2610
|
+
cleanup();
|
|
2611
|
+
fail(new Error("Cannot access relay WebSocket"));
|
|
2612
|
+
return Effect15.succeed(void 0);
|
|
2613
|
+
}
|
|
2614
|
+
timer = setTimeout(() => {
|
|
2615
|
+
if (settled) return;
|
|
2616
|
+
cleanup();
|
|
2617
|
+
fail(new Error("NIP-77 negotiation timeout"));
|
|
2618
|
+
}, 3e4);
|
|
2619
|
+
ws.addEventListener("message", handler);
|
|
2620
|
+
ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
|
|
2621
|
+
} catch (e) {
|
|
2265
2622
|
cleanup();
|
|
2266
|
-
fail(
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
cleanup();
|
|
2272
|
-
fail(e);
|
|
2273
|
-
}
|
|
2274
|
-
return Effect15.sync(cleanup);
|
|
2275
|
-
})),
|
|
2623
|
+
fail(e);
|
|
2624
|
+
}
|
|
2625
|
+
return Effect15.sync(cleanup);
|
|
2626
|
+
})
|
|
2627
|
+
),
|
|
2276
2628
|
closeAll: () => ScopedCache.invalidateAll(connections),
|
|
2277
2629
|
getStatus: () => ({ connectedUrls: [...connectedUrls] }),
|
|
2278
2630
|
subscribeStatus: (callback) => {
|
|
@@ -2324,11 +2676,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
|
2324
2676
|
}
|
|
2325
2677
|
|
|
2326
2678
|
// src/layers/GiftWrapLive.ts
|
|
2327
|
-
var GiftWrapLive = Layer6.effect(
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2679
|
+
var GiftWrapLive = Layer6.effect(
|
|
2680
|
+
GiftWrap3,
|
|
2681
|
+
Effect17.gen(function* () {
|
|
2682
|
+
const identity = yield* Identity;
|
|
2683
|
+
const epochStore = yield* EpochStore;
|
|
2684
|
+
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2685
|
+
})
|
|
2686
|
+
);
|
|
2332
2687
|
|
|
2333
2688
|
// src/layers/PublishQueueLive.ts
|
|
2334
2689
|
import { Effect as Effect19, Layer as Layer7 } from "effect";
|
|
@@ -2342,12 +2697,11 @@ function persist(storage, pending) {
|
|
|
2342
2697
|
function createPublishQueue(storage, relay) {
|
|
2343
2698
|
return Effect18.gen(function* () {
|
|
2344
2699
|
const stored = yield* storage.getMeta(META_KEY);
|
|
2345
|
-
const initial = Array.isArray(stored) ? new Set(stored) : new Set;
|
|
2700
|
+
const initial = Array.isArray(stored) ? new Set(stored) : /* @__PURE__ */ new Set();
|
|
2346
2701
|
const pendingRef = yield* Ref4.make(initial);
|
|
2347
|
-
const listeners = new Set;
|
|
2702
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2348
2703
|
const notify = (pending) => {
|
|
2349
|
-
for (const listener of listeners)
|
|
2350
|
-
listener(pending.size);
|
|
2704
|
+
for (const listener of listeners) listener(pending.size);
|
|
2351
2705
|
};
|
|
2352
2706
|
return {
|
|
2353
2707
|
enqueue: (eventId) => Effect18.gen(function* () {
|
|
@@ -2361,13 +2715,11 @@ function createPublishQueue(storage, relay) {
|
|
|
2361
2715
|
}),
|
|
2362
2716
|
flush: (relayUrls) => Effect18.gen(function* () {
|
|
2363
2717
|
const pending = yield* Ref4.get(pendingRef);
|
|
2364
|
-
if (pending.size === 0)
|
|
2365
|
-
|
|
2366
|
-
const succeeded = new Set;
|
|
2718
|
+
if (pending.size === 0) return;
|
|
2719
|
+
const succeeded = /* @__PURE__ */ new Set();
|
|
2367
2720
|
let consecutiveFailures = 0;
|
|
2368
2721
|
for (const eventId of pending) {
|
|
2369
|
-
if (consecutiveFailures >= 3)
|
|
2370
|
-
break;
|
|
2722
|
+
if (consecutiveFailures >= 3) break;
|
|
2371
2723
|
const gw = yield* storage.getGiftWrap(eventId);
|
|
2372
2724
|
if (!gw || !gw.event) {
|
|
2373
2725
|
succeeded.add(eventId);
|
|
@@ -2377,7 +2729,6 @@ function createPublishQueue(storage, relay) {
|
|
|
2377
2729
|
const result = yield* Effect18.result(relay.publish(gw.event, relayUrls));
|
|
2378
2730
|
if (result._tag === "Success") {
|
|
2379
2731
|
succeeded.add(eventId);
|
|
2380
|
-
yield* storage.stripGiftWrapBlob(eventId);
|
|
2381
2732
|
consecutiveFailures = 0;
|
|
2382
2733
|
} else {
|
|
2383
2734
|
consecutiveFailures++;
|
|
@@ -2405,11 +2756,14 @@ function createPublishQueue(storage, relay) {
|
|
|
2405
2756
|
}
|
|
2406
2757
|
|
|
2407
2758
|
// src/layers/PublishQueueLive.ts
|
|
2408
|
-
var PublishQueueLive = Layer7.effect(
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2759
|
+
var PublishQueueLive = Layer7.effect(
|
|
2760
|
+
PublishQueue,
|
|
2761
|
+
Effect19.gen(function* () {
|
|
2762
|
+
const storage = yield* Storage;
|
|
2763
|
+
const relay = yield* Relay;
|
|
2764
|
+
return yield* createPublishQueue(storage, relay);
|
|
2765
|
+
})
|
|
2766
|
+
);
|
|
2413
2767
|
|
|
2414
2768
|
// src/layers/SyncStatusLive.ts
|
|
2415
2769
|
import { Layer as Layer8 } from "effect";
|
|
@@ -2419,13 +2773,12 @@ import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
|
2419
2773
|
function createSyncStatusHandle() {
|
|
2420
2774
|
return Effect20.gen(function* () {
|
|
2421
2775
|
const ref = yield* SubscriptionRef.make("idle");
|
|
2422
|
-
const listeners = new Set;
|
|
2776
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2423
2777
|
return {
|
|
2424
2778
|
get: () => SubscriptionRef.get(ref),
|
|
2425
2779
|
set: (status) => Effect20.gen(function* () {
|
|
2426
2780
|
yield* SubscriptionRef.set(ref, status);
|
|
2427
|
-
for (const listener of listeners)
|
|
2428
|
-
listener(status);
|
|
2781
|
+
for (const listener of listeners) listener(status);
|
|
2429
2782
|
}),
|
|
2430
2783
|
subscribe: (callback) => {
|
|
2431
2784
|
listeners.add(callback);
|
|
@@ -2440,8 +2793,7 @@ var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
|
|
|
2440
2793
|
|
|
2441
2794
|
// src/layers/TablinumLive.ts
|
|
2442
2795
|
function reportSyncError(onSyncError, error) {
|
|
2443
|
-
if (!onSyncError)
|
|
2444
|
-
return;
|
|
2796
|
+
if (!onSyncError) return;
|
|
2445
2797
|
onSyncError(error instanceof Error ? error : new Error(String(error)));
|
|
2446
2798
|
}
|
|
2447
2799
|
function mapMemberRecord(record) {
|
|
@@ -2449,249 +2801,352 @@ function mapMemberRecord(record) {
|
|
|
2449
2801
|
id: record.id,
|
|
2450
2802
|
addedAt: record.addedAt,
|
|
2451
2803
|
addedInEpoch: record.addedInEpoch,
|
|
2452
|
-
...record.name !==
|
|
2453
|
-
...record.picture !==
|
|
2454
|
-
...record.about !==
|
|
2455
|
-
...record.nip05 !==
|
|
2456
|
-
...record.removedAt !==
|
|
2457
|
-
...record.removedInEpoch !==
|
|
2804
|
+
...record.name !== void 0 ? { name: record.name } : {},
|
|
2805
|
+
...record.picture !== void 0 ? { picture: record.picture } : {},
|
|
2806
|
+
...record.about !== void 0 ? { about: record.about } : {},
|
|
2807
|
+
...record.nip05 !== void 0 ? { nip05: record.nip05 } : {},
|
|
2808
|
+
...record.removedAt !== void 0 ? { removedAt: record.removedAt } : {},
|
|
2809
|
+
...record.removedInEpoch !== void 0 ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2458
2810
|
};
|
|
2459
2811
|
}
|
|
2460
2812
|
var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
|
|
2461
|
-
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
var
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
const
|
|
2487
|
-
const
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
const
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2813
|
+
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2814
|
+
Layer9.provide(IdentityWithDeps),
|
|
2815
|
+
Layer9.provide(StorageLive)
|
|
2816
|
+
);
|
|
2817
|
+
var GiftWrapWithDeps = GiftWrapLive.pipe(
|
|
2818
|
+
Layer9.provide(IdentityWithDeps),
|
|
2819
|
+
Layer9.provide(EpochStoreWithDeps)
|
|
2820
|
+
);
|
|
2821
|
+
var PublishQueueWithDeps = PublishQueueLive.pipe(
|
|
2822
|
+
Layer9.provide(StorageLive),
|
|
2823
|
+
Layer9.provide(RelayLive)
|
|
2824
|
+
);
|
|
2825
|
+
var AllServicesLive = Layer9.mergeAll(
|
|
2826
|
+
IdentityWithDeps,
|
|
2827
|
+
EpochStoreWithDeps,
|
|
2828
|
+
StorageLive,
|
|
2829
|
+
RelayLive,
|
|
2830
|
+
GiftWrapWithDeps,
|
|
2831
|
+
PublishQueueWithDeps,
|
|
2832
|
+
SyncStatusLive
|
|
2833
|
+
);
|
|
2834
|
+
var TablinumLive = Layer9.effect(
|
|
2835
|
+
Tablinum,
|
|
2836
|
+
Effect21.gen(function* () {
|
|
2837
|
+
const config = yield* Config;
|
|
2838
|
+
const identity = yield* Identity;
|
|
2839
|
+
const epochStore = yield* EpochStore;
|
|
2840
|
+
const storage = yield* Storage;
|
|
2841
|
+
const relay = yield* Relay;
|
|
2842
|
+
const giftWrap = yield* GiftWrap3;
|
|
2843
|
+
const publishQueue = yield* PublishQueue;
|
|
2844
|
+
const syncStatus = yield* SyncStatus;
|
|
2845
|
+
const scope = yield* Effect21.scope;
|
|
2846
|
+
const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
|
|
2847
|
+
const pubsub = yield* PubSub2.unbounded();
|
|
2848
|
+
const replayingRef = yield* Ref5.make(false);
|
|
2849
|
+
const closedRef = yield* Ref5.make(false);
|
|
2850
|
+
const watchCtx = { pubsub, replayingRef };
|
|
2851
|
+
const schemaEntries = Object.entries(config.schema);
|
|
2852
|
+
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2853
|
+
const knownCollections = new Map(
|
|
2854
|
+
allSchemaEntries.map(([, def]) => [def.name, def.eventRetention])
|
|
2855
|
+
);
|
|
2856
|
+
let notifyAuthor;
|
|
2857
|
+
const syncHandle = createSyncHandle(
|
|
2858
|
+
storage,
|
|
2859
|
+
giftWrap,
|
|
2860
|
+
relay,
|
|
2861
|
+
publishQueue,
|
|
2862
|
+
syncStatus,
|
|
2863
|
+
watchCtx,
|
|
2864
|
+
config.relays,
|
|
2865
|
+
knownCollections,
|
|
2866
|
+
epochStore,
|
|
2867
|
+
identity.privateKey,
|
|
2868
|
+
identity.publicKey,
|
|
2869
|
+
scope,
|
|
2870
|
+
config.logLevel,
|
|
2871
|
+
config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : void 0,
|
|
2872
|
+
(pubkey) => notifyAuthor?.(pubkey),
|
|
2873
|
+
config.onRemoved,
|
|
2874
|
+
config.onMembersChanged
|
|
2875
|
+
);
|
|
2876
|
+
const onWrite = (event) => Effect21.gen(function* () {
|
|
2877
|
+
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2878
|
+
const dTag = `${event.collection}:${event.recordId}`;
|
|
2879
|
+
const wrapResult = yield* Effect21.result(
|
|
2880
|
+
giftWrap.wrap({
|
|
2881
|
+
kind: 1,
|
|
2882
|
+
content,
|
|
2883
|
+
tags: [["d", dTag]],
|
|
2884
|
+
created_at: Math.floor(event.createdAt / 1e3)
|
|
2885
|
+
})
|
|
2886
|
+
);
|
|
2887
|
+
if (wrapResult._tag === "Failure") {
|
|
2888
|
+
reportSyncError(config.onSyncError, wrapResult.failure);
|
|
2889
|
+
return;
|
|
2508
2890
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
yield* notifyChange(watchCtx, {
|
|
2527
|
-
collection: "_members",
|
|
2528
|
-
recordId: record.id,
|
|
2529
|
-
kind: existing ? "update" : "create"
|
|
2891
|
+
const gw = wrapResult.success;
|
|
2892
|
+
yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
2893
|
+
yield* Effect21.forkIn(
|
|
2894
|
+
Effect21.gen(function* () {
|
|
2895
|
+
const publishResult = yield* Effect21.result(
|
|
2896
|
+
syncHandle.publishLocal({
|
|
2897
|
+
id: gw.id,
|
|
2898
|
+
event: gw,
|
|
2899
|
+
createdAt: gw.created_at
|
|
2900
|
+
})
|
|
2901
|
+
);
|
|
2902
|
+
if (publishResult._tag === "Failure") {
|
|
2903
|
+
reportSyncError(config.onSyncError, publishResult.failure);
|
|
2904
|
+
}
|
|
2905
|
+
}),
|
|
2906
|
+
scope
|
|
2907
|
+
);
|
|
2530
2908
|
});
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
...profileOpt.value
|
|
2553
|
-
});
|
|
2554
|
-
yield* notifyChange(watchCtx, {
|
|
2555
|
-
collection: "_members",
|
|
2556
|
-
recordId: pubkey,
|
|
2557
|
-
kind: "update"
|
|
2558
|
-
});
|
|
2559
|
-
config.onMembersChanged?.();
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope)));
|
|
2563
|
-
};
|
|
2564
|
-
const handles = new Map;
|
|
2565
|
-
for (const [, def] of allSchemaEntries) {
|
|
2566
|
-
const validator = buildValidator(def.name, def);
|
|
2567
|
-
const partialValidator = buildPartialValidator(def.name, def);
|
|
2568
|
-
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite, config.logLevel);
|
|
2569
|
-
handles.set(def.name, handle);
|
|
2570
|
-
}
|
|
2571
|
-
yield* syncHandle.startSubscription();
|
|
2572
|
-
yield* Effect21.logInfo("Tablinum ready", {
|
|
2573
|
-
dbName: config.dbName,
|
|
2574
|
-
collections: schemaEntries.map(([name]) => name),
|
|
2575
|
-
relays: config.relays
|
|
2576
|
-
});
|
|
2577
|
-
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2578
|
-
if (!selfMember) {
|
|
2579
|
-
yield* putMemberRecord({
|
|
2580
|
-
id: identity.publicKey,
|
|
2581
|
-
addedAt: Date.now(),
|
|
2582
|
-
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2909
|
+
const knownAuthors = /* @__PURE__ */ new Set();
|
|
2910
|
+
const putMemberRecord = (record) => Effect21.gen(function* () {
|
|
2911
|
+
const existing = yield* storage.getRecord("_members", record.id);
|
|
2912
|
+
const event = {
|
|
2913
|
+
id: uuidv7(),
|
|
2914
|
+
collection: "_members",
|
|
2915
|
+
recordId: record.id,
|
|
2916
|
+
kind: existing ? "u" : "c",
|
|
2917
|
+
data: record,
|
|
2918
|
+
createdAt: Date.now(),
|
|
2919
|
+
author: identity.publicKey
|
|
2920
|
+
};
|
|
2921
|
+
yield* storage.putEvent(event);
|
|
2922
|
+
yield* applyEvent(storage, event);
|
|
2923
|
+
yield* onWrite(event);
|
|
2924
|
+
yield* notifyChange(watchCtx, {
|
|
2925
|
+
collection: "_members",
|
|
2926
|
+
recordId: record.id,
|
|
2927
|
+
kind: existing ? "update" : "create"
|
|
2928
|
+
});
|
|
2929
|
+
config.onMembersChanged?.();
|
|
2583
2930
|
});
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2931
|
+
notifyAuthor = (pubkey) => {
|
|
2932
|
+
if (knownAuthors.has(pubkey)) return;
|
|
2933
|
+
knownAuthors.add(pubkey);
|
|
2934
|
+
Effect21.runFork(
|
|
2935
|
+
Effect21.gen(function* () {
|
|
2936
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
2937
|
+
if (!existing) {
|
|
2938
|
+
yield* putMemberRecord({
|
|
2939
|
+
id: pubkey,
|
|
2940
|
+
addedAt: Date.now(),
|
|
2941
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(
|
|
2945
|
+
Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none()))
|
|
2946
|
+
);
|
|
2947
|
+
if (Option9.isSome(profileOpt)) {
|
|
2948
|
+
const current = yield* storage.getRecord("_members", pubkey);
|
|
2949
|
+
if (current) {
|
|
2950
|
+
yield* storage.putRecord("_members", {
|
|
2951
|
+
...current,
|
|
2952
|
+
...profileOpt.value
|
|
2953
|
+
});
|
|
2954
|
+
yield* notifyChange(watchCtx, {
|
|
2955
|
+
collection: "_members",
|
|
2956
|
+
recordId: pubkey,
|
|
2957
|
+
kind: "update"
|
|
2958
|
+
});
|
|
2959
|
+
config.onMembersChanged?.();
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope))
|
|
2963
|
+
);
|
|
2964
|
+
};
|
|
2965
|
+
const handles = /* @__PURE__ */ new Map();
|
|
2966
|
+
for (const [, def] of allSchemaEntries) {
|
|
2967
|
+
const validator = buildValidator(def.name, def);
|
|
2968
|
+
const partialValidator = buildPartialValidator(def.name, def);
|
|
2969
|
+
const handle = createCollectionHandle(
|
|
2970
|
+
def,
|
|
2971
|
+
storage,
|
|
2972
|
+
watchCtx,
|
|
2973
|
+
validator,
|
|
2974
|
+
partialValidator,
|
|
2975
|
+
uuidv7,
|
|
2976
|
+
identity.publicKey,
|
|
2977
|
+
onWrite,
|
|
2978
|
+
config.logLevel
|
|
2979
|
+
);
|
|
2980
|
+
handles.set(def.name, handle);
|
|
2981
|
+
}
|
|
2982
|
+
yield* syncHandle.startSubscription();
|
|
2983
|
+
yield* Effect21.logInfo("Tablinum ready", {
|
|
2984
|
+
dbName: config.dbName,
|
|
2985
|
+
collections: schemaEntries.map(([name]) => name),
|
|
2986
|
+
relays: config.relays
|
|
2987
|
+
});
|
|
2988
|
+
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2989
|
+
if (!selfMember) {
|
|
2631
2990
|
yield* putMemberRecord({
|
|
2632
|
-
id:
|
|
2991
|
+
id: identity.publicKey,
|
|
2633
2992
|
addedAt: Date.now(),
|
|
2634
|
-
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2635
|
-
...existing ? { removedAt: undefined, removedInEpoch: undefined } : {}
|
|
2636
|
-
});
|
|
2637
|
-
})),
|
|
2638
|
-
removeMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
|
|
2639
|
-
const allMembers = yield* storage.getAllRecords("_members");
|
|
2640
|
-
const activeMembers = allMembers.filter((member) => !member.removedAt && member.id !== pubkey);
|
|
2641
|
-
const activePubkeys = activeMembers.map((member) => member.id);
|
|
2642
|
-
const result = createRotation(epochStore, identity.privateKey, identity.publicKey, activePubkeys, [pubkey]);
|
|
2643
|
-
addEpoch(epochStore, result.epoch);
|
|
2644
|
-
epochStore.currentEpochId = result.epoch.id;
|
|
2645
|
-
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
2646
|
-
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
2647
|
-
yield* putMemberRecord({
|
|
2648
|
-
...memberRecord ?? {
|
|
2649
|
-
id: pubkey,
|
|
2650
|
-
addedAt: 0,
|
|
2651
|
-
addedInEpoch: EpochId("epoch-0")
|
|
2652
|
-
},
|
|
2653
|
-
removedAt: Date.now(),
|
|
2654
|
-
removedInEpoch: result.epoch.id
|
|
2993
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2655
2994
|
});
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2995
|
+
}
|
|
2996
|
+
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
2997
|
+
const ensureOpen = (effect) => withLog(
|
|
2998
|
+
Effect21.gen(function* () {
|
|
2999
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3000
|
+
return yield* new StorageError({ message: "Database is closed" });
|
|
3001
|
+
}
|
|
3002
|
+
return yield* effect;
|
|
3003
|
+
})
|
|
3004
|
+
);
|
|
3005
|
+
const ensureSyncOpen = (effect) => withLog(
|
|
3006
|
+
Effect21.gen(function* () {
|
|
3007
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3008
|
+
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
3009
|
+
}
|
|
3010
|
+
return yield* effect;
|
|
3011
|
+
})
|
|
3012
|
+
);
|
|
3013
|
+
const dbHandle = {
|
|
3014
|
+
collection: (name) => {
|
|
3015
|
+
const handle = handles.get(name);
|
|
3016
|
+
if (!handle) throw new Error(`Collection "${name}" not found in schema`);
|
|
3017
|
+
return handle;
|
|
3018
|
+
},
|
|
3019
|
+
publicKey: identity.publicKey,
|
|
3020
|
+
members: handles.get("_members"),
|
|
3021
|
+
exportKey: () => identity.exportKey(),
|
|
3022
|
+
exportInvite: () => ({
|
|
3023
|
+
epochKeys: [...exportEpochKeys(epochStore)],
|
|
3024
|
+
relays: [...config.relays],
|
|
3025
|
+
dbName: config.dbName
|
|
3026
|
+
}),
|
|
3027
|
+
close: () => withLog(
|
|
3028
|
+
Effect21.gen(function* () {
|
|
3029
|
+
if (yield* Ref5.get(closedRef)) return;
|
|
3030
|
+
yield* Ref5.set(closedRef, true);
|
|
3031
|
+
syncHandle.stopHealing();
|
|
3032
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3033
|
+
})
|
|
3034
|
+
),
|
|
3035
|
+
rebuild: () => ensureOpen(
|
|
3036
|
+
rebuild(
|
|
3037
|
+
storage,
|
|
3038
|
+
allSchemaEntries.map(([, def]) => def.name)
|
|
3039
|
+
)
|
|
3040
|
+
),
|
|
3041
|
+
sync: () => ensureSyncOpen(
|
|
3042
|
+
syncHandle.sync().pipe(
|
|
3043
|
+
Effect21.tap(
|
|
3044
|
+
() => Effect21.sync(() => {
|
|
3045
|
+
syncHandle.startHealing();
|
|
3046
|
+
})
|
|
3047
|
+
)
|
|
3048
|
+
)
|
|
3049
|
+
),
|
|
3050
|
+
getSyncStatus: () => syncStatus.get(),
|
|
3051
|
+
subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
|
|
3052
|
+
pendingCount: () => publishQueue.size(),
|
|
3053
|
+
subscribePendingCount: (callback) => publishQueue.subscribe(callback),
|
|
3054
|
+
getRelayStatus: () => relay.getStatus(),
|
|
3055
|
+
subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
|
|
3056
|
+
addMember: (pubkey) => ensureOpen(
|
|
3057
|
+
Effect21.gen(function* () {
|
|
3058
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
3059
|
+
if (existing && !existing.removedAt) return;
|
|
3060
|
+
yield* putMemberRecord({
|
|
3061
|
+
id: pubkey,
|
|
3062
|
+
addedAt: Date.now(),
|
|
3063
|
+
addedInEpoch: getCurrentEpoch(epochStore).id,
|
|
3064
|
+
...existing ? { removedAt: void 0, removedInEpoch: void 0 } : {}
|
|
3065
|
+
});
|
|
3066
|
+
})
|
|
3067
|
+
),
|
|
3068
|
+
removeMember: (pubkey) => ensureOpen(
|
|
3069
|
+
Effect21.gen(function* () {
|
|
3070
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
3071
|
+
const activeMembers = allMembers.filter(
|
|
3072
|
+
(member) => !member.removedAt && member.id !== pubkey
|
|
3073
|
+
);
|
|
3074
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
3075
|
+
const result = createRotation(
|
|
3076
|
+
epochStore,
|
|
3077
|
+
identity.privateKey,
|
|
3078
|
+
identity.publicKey,
|
|
3079
|
+
activePubkeys,
|
|
3080
|
+
[pubkey]
|
|
3081
|
+
);
|
|
3082
|
+
addEpoch(epochStore, result.epoch);
|
|
3083
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
3084
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
3085
|
+
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
3086
|
+
yield* putMemberRecord({
|
|
3087
|
+
...memberRecord ?? {
|
|
3088
|
+
id: pubkey,
|
|
3089
|
+
addedAt: 0,
|
|
3090
|
+
addedInEpoch: EpochId("epoch-0")
|
|
3091
|
+
},
|
|
3092
|
+
removedAt: Date.now(),
|
|
3093
|
+
removedInEpoch: result.epoch.id
|
|
3094
|
+
});
|
|
3095
|
+
yield* Effect21.forEach(
|
|
3096
|
+
result.wrappedEvents,
|
|
3097
|
+
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
3098
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3099
|
+
Effect21.ignore
|
|
3100
|
+
),
|
|
3101
|
+
{ discard: true }
|
|
3102
|
+
);
|
|
3103
|
+
yield* Effect21.forEach(
|
|
3104
|
+
result.removalNotices,
|
|
3105
|
+
(notice) => relay.publish(notice, [...config.relays]).pipe(
|
|
3106
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3107
|
+
Effect21.ignore
|
|
3108
|
+
),
|
|
3109
|
+
{ discard: true }
|
|
3110
|
+
);
|
|
3111
|
+
yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
|
|
3112
|
+
})
|
|
3113
|
+
),
|
|
3114
|
+
getMembers: () => ensureOpen(
|
|
3115
|
+
Effect21.gen(function* () {
|
|
3116
|
+
const allRecords = yield* storage.getAllRecords("_members");
|
|
3117
|
+
return allRecords.filter((record) => !record._d).map(mapMemberRecord);
|
|
3118
|
+
})
|
|
3119
|
+
),
|
|
3120
|
+
getProfile: () => ensureOpen(
|
|
3121
|
+
Effect21.gen(function* () {
|
|
3122
|
+
const record = yield* storage.getRecord("_members", identity.publicKey);
|
|
3123
|
+
if (!record) return {};
|
|
3124
|
+
const profile = {};
|
|
3125
|
+
if (record.name !== void 0) profile.name = record.name;
|
|
3126
|
+
if (record.picture !== void 0) profile.picture = record.picture;
|
|
3127
|
+
if (record.about !== void 0) profile.about = record.about;
|
|
3128
|
+
if (record.nip05 !== void 0) profile.nip05 = record.nip05;
|
|
3129
|
+
return profile;
|
|
3130
|
+
})
|
|
3131
|
+
),
|
|
3132
|
+
setProfile: (profile) => ensureOpen(
|
|
3133
|
+
Effect21.gen(function* () {
|
|
3134
|
+
const existing = yield* storage.getRecord("_members", identity.publicKey);
|
|
3135
|
+
if (!existing) {
|
|
3136
|
+
return yield* new ValidationError({ message: "Current user is not a member" });
|
|
3137
|
+
}
|
|
3138
|
+
const { _d, _u, _a, _e, ...memberFields } = existing;
|
|
3139
|
+
yield* putMemberRecord({ ...memberFields, ...profile });
|
|
3140
|
+
})
|
|
3141
|
+
)
|
|
3142
|
+
};
|
|
3143
|
+
return dbHandle;
|
|
3144
|
+
}).pipe(Effect21.withLogSpan("tablinum.init"))
|
|
3145
|
+
).pipe(Layer9.provide(AllServicesLive));
|
|
2690
3146
|
|
|
2691
3147
|
// src/db/create-tablinum.ts
|
|
2692
3148
|
function resolveLogLevel(input) {
|
|
2693
|
-
if (input ===
|
|
2694
|
-
return "None";
|
|
3149
|
+
if (input === void 0 || input === "none") return "None";
|
|
2695
3150
|
switch (input) {
|
|
2696
3151
|
case "debug":
|
|
2697
3152
|
return "Debug";
|
|
@@ -2772,20 +3227,20 @@ function decodeInvite(encoded) {
|
|
|
2772
3227
|
}
|
|
2773
3228
|
}
|
|
2774
3229
|
export {
|
|
2775
|
-
|
|
2776
|
-
encodeInvite,
|
|
2777
|
-
decodeInvite,
|
|
2778
|
-
createTablinum,
|
|
2779
|
-
collection,
|
|
2780
|
-
ValidationError,
|
|
2781
|
-
TablinumLive,
|
|
2782
|
-
SyncError,
|
|
2783
|
-
StorageError,
|
|
2784
|
-
RelayError,
|
|
2785
|
-
NotFoundError,
|
|
2786
|
-
LogLevel,
|
|
2787
|
-
EpochId,
|
|
2788
|
-
DatabaseName,
|
|
3230
|
+
ClosedError,
|
|
2789
3231
|
CryptoError,
|
|
2790
|
-
|
|
3232
|
+
DatabaseName,
|
|
3233
|
+
EpochId,
|
|
3234
|
+
LogLevel,
|
|
3235
|
+
NotFoundError,
|
|
3236
|
+
RelayError,
|
|
3237
|
+
StorageError,
|
|
3238
|
+
SyncError,
|
|
3239
|
+
TablinumLive,
|
|
3240
|
+
ValidationError,
|
|
3241
|
+
collection,
|
|
3242
|
+
createTablinum,
|
|
3243
|
+
decodeInvite,
|
|
3244
|
+
encodeInvite,
|
|
3245
|
+
field
|
|
2791
3246
|
};
|