tablinum 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crud/collection-handle.d.ts +2 -1
- package/dist/db/create-tablinum.d.ts +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1383 -907
- package/dist/services/Config.d.ts +2 -0
- package/dist/storage/idb.d.ts +0 -1
- package/dist/svelte/collection.svelte.d.ts +2 -1
- package/dist/svelte/index.svelte.js +1458 -952
- package/dist/sync/compact-event.d.ts +3 -0
- package/dist/sync/sync-service.d.ts +4 -1
- package/package.json +8 -27
- package/README.md +0 -238
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,35 +55,29 @@ 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
|
-
import { Effect as Effect22, Layer as
|
|
60
|
+
import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
|
|
58
61
|
|
|
59
62
|
// src/db/runtime-config.ts
|
|
60
63
|
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
|
-
import { Effect as Effect21, Exit, Layer as
|
|
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
|
});
|
|
@@ -407,9 +428,16 @@ function buildStructSchema(def, options = {}) {
|
|
|
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,35 +449,38 @@ 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
|
|
|
436
469
|
// src/crud/collection-handle.ts
|
|
437
|
-
import { Effect as Effect6, Option as Option3 } from "effect";
|
|
470
|
+
import { Effect as Effect6, Option as Option3, References } from "effect";
|
|
438
471
|
|
|
439
472
|
// src/utils/uuid.ts
|
|
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
|
}
|
|
@@ -682,13 +754,13 @@ function mapRecord(record) {
|
|
|
682
754
|
const { _d, _u, _a, _e, ...fields } = record;
|
|
683
755
|
return fields;
|
|
684
756
|
}
|
|
685
|
-
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite) {
|
|
757
|
+
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
686
758
|
const collectionName = def.name;
|
|
759
|
+
const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
687
760
|
const commitEvent = (event) => Effect6.gen(function* () {
|
|
688
761
|
yield* storage.putEvent(event);
|
|
689
762
|
yield* applyEvent(storage, event);
|
|
690
|
-
if (onWrite)
|
|
691
|
-
yield* onWrite(event);
|
|
763
|
+
if (onWrite) yield* onWrite(event);
|
|
692
764
|
yield* notifyChange(watchCtx, {
|
|
693
765
|
collection: collectionName,
|
|
694
766
|
recordId: event.recordId,
|
|
@@ -696,67 +768,84 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
696
768
|
});
|
|
697
769
|
});
|
|
698
770
|
const handle = {
|
|
699
|
-
add: (data) =>
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
recordId: id,
|
|
707
|
-
kind: "c",
|
|
708
|
-
data: fullRecord,
|
|
709
|
-
createdAt: Date.now(),
|
|
710
|
-
author: localAuthor
|
|
711
|
-
};
|
|
712
|
-
yield* commitEvent(event);
|
|
713
|
-
return id;
|
|
714
|
-
}),
|
|
715
|
-
update: (id, data) => Effect6.gen(function* () {
|
|
716
|
-
const existing = yield* storage.getRecord(collectionName, id);
|
|
717
|
-
if (!existing || existing._d) {
|
|
718
|
-
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(),
|
|
719
778
|
collection: collectionName,
|
|
720
|
-
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
|
|
721
790
|
});
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const existing = yield* storage.getRecord(collectionName, id);
|
|
742
|
-
if (!existing || existing._d) {
|
|
743
|
-
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(),
|
|
744
810
|
collection: collectionName,
|
|
745
|
-
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
|
|
746
822
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
+
),
|
|
760
849
|
undo: (id) => Effect6.gen(function* () {
|
|
761
850
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
762
851
|
if (!existing) {
|
|
@@ -797,15 +886,29 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
797
886
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
798
887
|
}),
|
|
799
888
|
count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
800
|
-
watch: () => watchCollection(watchCtx, storage, collectionName,
|
|
801
|
-
where: (fieldName) => createWhereClause(
|
|
802
|
-
|
|
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
|
+
)
|
|
803
906
|
};
|
|
804
907
|
return handle;
|
|
805
908
|
}
|
|
806
909
|
|
|
807
910
|
// src/sync/sync-service.ts
|
|
808
|
-
import { Effect as Effect8, Option as Option5, 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";
|
|
809
912
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
810
913
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
811
914
|
|
|
@@ -821,8 +924,7 @@ var Mode = {
|
|
|
821
924
|
Fingerprint: 1,
|
|
822
925
|
IdList: 2
|
|
823
926
|
};
|
|
824
|
-
|
|
825
|
-
class WrappedBuffer {
|
|
927
|
+
var WrappedBuffer = class {
|
|
826
928
|
constructor(buffer) {
|
|
827
929
|
this._raw = new Uint8Array(buffer || 512);
|
|
828
930
|
this.length = buffer ? buffer.length : 0;
|
|
@@ -834,10 +936,8 @@ class WrappedBuffer {
|
|
|
834
936
|
return this._raw.byteLength;
|
|
835
937
|
}
|
|
836
938
|
extend(buf) {
|
|
837
|
-
if (buf._raw)
|
|
838
|
-
|
|
839
|
-
if (typeof buf.length !== "number")
|
|
840
|
-
throw Error("bad length");
|
|
939
|
+
if (buf._raw) buf = buf.unwrap();
|
|
940
|
+
if (typeof buf.length !== "number") throw Error("bad length");
|
|
841
941
|
const targetSize = buf.length + this.length;
|
|
842
942
|
if (this.capacity < targetSize) {
|
|
843
943
|
const oldRaw = this._raw;
|
|
@@ -860,42 +960,36 @@ class WrappedBuffer {
|
|
|
860
960
|
this.length -= n;
|
|
861
961
|
return firstSubarray;
|
|
862
962
|
}
|
|
863
|
-
}
|
|
963
|
+
};
|
|
864
964
|
function decodeVarInt(buf) {
|
|
865
965
|
let res = 0;
|
|
866
|
-
while (
|
|
867
|
-
if (buf.length === 0)
|
|
868
|
-
throw Error("parse ends prematurely");
|
|
966
|
+
while (1) {
|
|
967
|
+
if (buf.length === 0) throw Error("parse ends prematurely");
|
|
869
968
|
let byte = buf.shift();
|
|
870
969
|
res = res << 7 | byte & 127;
|
|
871
|
-
if ((byte & 128) === 0)
|
|
872
|
-
break;
|
|
970
|
+
if ((byte & 128) === 0) break;
|
|
873
971
|
}
|
|
874
972
|
return res;
|
|
875
973
|
}
|
|
876
974
|
function encodeVarInt(n) {
|
|
877
|
-
if (n === 0)
|
|
878
|
-
return new WrappedBuffer([0]);
|
|
975
|
+
if (n === 0) return new WrappedBuffer([0]);
|
|
879
976
|
let o = [];
|
|
880
977
|
while (n !== 0) {
|
|
881
978
|
o.push(n & 127);
|
|
882
979
|
n >>>= 7;
|
|
883
980
|
}
|
|
884
981
|
o.reverse();
|
|
885
|
-
for (let i = 0;i < o.length - 1; i++)
|
|
886
|
-
o[i] |= 128;
|
|
982
|
+
for (let i = 0; i < o.length - 1; i++) o[i] |= 128;
|
|
887
983
|
return new WrappedBuffer(o);
|
|
888
984
|
}
|
|
889
985
|
function getByte(buf) {
|
|
890
986
|
return getBytes(buf, 1)[0];
|
|
891
987
|
}
|
|
892
988
|
function getBytes(buf, n) {
|
|
893
|
-
if (buf.length < n)
|
|
894
|
-
throw Error("parse ends prematurely");
|
|
989
|
+
if (buf.length < n) throw Error("parse ends prematurely");
|
|
895
990
|
return buf.shiftN(n);
|
|
896
991
|
}
|
|
897
|
-
|
|
898
|
-
class Accumulator {
|
|
992
|
+
var Accumulator = class {
|
|
899
993
|
constructor() {
|
|
900
994
|
this.setToZero();
|
|
901
995
|
if (typeof window === "undefined") {
|
|
@@ -912,15 +1006,14 @@ class Accumulator {
|
|
|
912
1006
|
let currCarry = 0, nextCarry = 0;
|
|
913
1007
|
let p = new DataView(this.buf.buffer);
|
|
914
1008
|
let po = new DataView(otherBuf.buffer);
|
|
915
|
-
for (let i = 0;i < 8; i++) {
|
|
1009
|
+
for (let i = 0; i < 8; i++) {
|
|
916
1010
|
let offset = i * 4;
|
|
917
1011
|
let orig = p.getUint32(offset, true);
|
|
918
1012
|
let otherV = po.getUint32(offset, true);
|
|
919
1013
|
let next = orig;
|
|
920
1014
|
next += currCarry;
|
|
921
1015
|
next += otherV;
|
|
922
|
-
if (next > 4294967295)
|
|
923
|
-
nextCarry = 1;
|
|
1016
|
+
if (next > 4294967295) nextCarry = 1;
|
|
924
1017
|
p.setUint32(offset, next & 4294967295, true);
|
|
925
1018
|
currCarry = nextCarry;
|
|
926
1019
|
nextCarry = 0;
|
|
@@ -928,7 +1021,7 @@ class Accumulator {
|
|
|
928
1021
|
}
|
|
929
1022
|
negate() {
|
|
930
1023
|
let p = new DataView(this.buf.buffer);
|
|
931
|
-
for (let i = 0;i < 8; i++) {
|
|
1024
|
+
for (let i = 0; i < 8; i++) {
|
|
932
1025
|
let offset = i * 4;
|
|
933
1026
|
p.setUint32(offset, ~p.getUint32(offset, true));
|
|
934
1027
|
}
|
|
@@ -937,33 +1030,29 @@ class Accumulator {
|
|
|
937
1030
|
this.add(one);
|
|
938
1031
|
}
|
|
939
1032
|
async getFingerprint(n) {
|
|
940
|
-
let input = new WrappedBuffer;
|
|
1033
|
+
let input = new WrappedBuffer();
|
|
941
1034
|
input.extend(this.buf);
|
|
942
1035
|
input.extend(encodeVarInt(n));
|
|
943
1036
|
let hash = await this.sha256(input.unwrap());
|
|
944
1037
|
return hash.subarray(0, FINGERPRINT_SIZE);
|
|
945
1038
|
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
class NegentropyStorageVector {
|
|
1039
|
+
};
|
|
1040
|
+
var NegentropyStorageVector = class {
|
|
949
1041
|
constructor() {
|
|
950
1042
|
this.items = [];
|
|
951
1043
|
this.sealed = false;
|
|
952
1044
|
}
|
|
953
1045
|
insert(timestamp, id) {
|
|
954
|
-
if (this.sealed)
|
|
955
|
-
throw Error("already sealed");
|
|
1046
|
+
if (this.sealed) throw Error("already sealed");
|
|
956
1047
|
id = loadInputBuffer(id);
|
|
957
|
-
if (id.byteLength !== ID_SIZE)
|
|
958
|
-
throw Error("bad id size for added item");
|
|
1048
|
+
if (id.byteLength !== ID_SIZE) throw Error("bad id size for added item");
|
|
959
1049
|
this.items.push({ timestamp, id });
|
|
960
1050
|
}
|
|
961
1051
|
seal() {
|
|
962
|
-
if (this.sealed)
|
|
963
|
-
throw Error("already sealed");
|
|
1052
|
+
if (this.sealed) throw Error("already sealed");
|
|
964
1053
|
this.sealed = true;
|
|
965
1054
|
this.items.sort(itemCompare);
|
|
966
|
-
for (let i = 1;i < this.items.length; i++) {
|
|
1055
|
+
for (let i = 1; i < this.items.length; i++) {
|
|
967
1056
|
if (itemCompare(this.items[i - 1], this.items[i]) === 0)
|
|
968
1057
|
throw Error("duplicate item inserted");
|
|
969
1058
|
}
|
|
@@ -977,16 +1066,14 @@ class NegentropyStorageVector {
|
|
|
977
1066
|
}
|
|
978
1067
|
getItem(i) {
|
|
979
1068
|
this._checkSealed();
|
|
980
|
-
if (i >= this.items.length)
|
|
981
|
-
throw Error("out of range");
|
|
1069
|
+
if (i >= this.items.length) throw Error("out of range");
|
|
982
1070
|
return this.items[i];
|
|
983
1071
|
}
|
|
984
1072
|
iterate(begin, end, cb) {
|
|
985
1073
|
this._checkSealed();
|
|
986
1074
|
this._checkBounds(begin, end);
|
|
987
|
-
for (let i = begin;i < end; ++i) {
|
|
988
|
-
if (!cb(this.items[i], i))
|
|
989
|
-
break;
|
|
1075
|
+
for (let i = begin; i < end; ++i) {
|
|
1076
|
+
if (!cb(this.items[i], i)) break;
|
|
990
1077
|
}
|
|
991
1078
|
}
|
|
992
1079
|
findLowerBound(begin, end, bound) {
|
|
@@ -995,7 +1082,7 @@ class NegentropyStorageVector {
|
|
|
995
1082
|
return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0);
|
|
996
1083
|
}
|
|
997
1084
|
async fingerprint(begin, end) {
|
|
998
|
-
let out = new Accumulator;
|
|
1085
|
+
let out = new Accumulator();
|
|
999
1086
|
out.setToZero();
|
|
1000
1087
|
this.iterate(begin, end, (item, i) => {
|
|
1001
1088
|
out.add(item.id);
|
|
@@ -1004,12 +1091,10 @@ class NegentropyStorageVector {
|
|
|
1004
1091
|
return await out.getFingerprint(end - begin);
|
|
1005
1092
|
}
|
|
1006
1093
|
_checkSealed() {
|
|
1007
|
-
if (!this.sealed)
|
|
1008
|
-
throw Error("not sealed");
|
|
1094
|
+
if (!this.sealed) throw Error("not sealed");
|
|
1009
1095
|
}
|
|
1010
1096
|
_checkBounds(begin, end) {
|
|
1011
|
-
if (begin > end || end > this.items.length)
|
|
1012
|
-
throw Error("bad range");
|
|
1097
|
+
if (begin > end || end > this.items.length) throw Error("bad range");
|
|
1013
1098
|
}
|
|
1014
1099
|
_binarySearch(arr, first, last, cmp) {
|
|
1015
1100
|
let count = last - first;
|
|
@@ -1026,12 +1111,10 @@ class NegentropyStorageVector {
|
|
|
1026
1111
|
}
|
|
1027
1112
|
return first;
|
|
1028
1113
|
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
class Negentropy {
|
|
1114
|
+
};
|
|
1115
|
+
var Negentropy = class {
|
|
1032
1116
|
constructor(storage, frameSizeLimit = 0) {
|
|
1033
|
-
if (frameSizeLimit !== 0 && frameSizeLimit < 4096)
|
|
1034
|
-
throw Error("frameSizeLimit too small");
|
|
1117
|
+
if (frameSizeLimit !== 0 && frameSizeLimit < 4096) throw Error("frameSizeLimit too small");
|
|
1035
1118
|
this.storage = storage;
|
|
1036
1119
|
this.frameSizeLimit = frameSizeLimit;
|
|
1037
1120
|
this.lastTimestampIn = 0;
|
|
@@ -1041,10 +1124,9 @@ class Negentropy {
|
|
|
1041
1124
|
return { timestamp, id: id ? id : new Uint8Array(0) };
|
|
1042
1125
|
}
|
|
1043
1126
|
async initiate() {
|
|
1044
|
-
if (this.isInitiator)
|
|
1045
|
-
throw Error("already initiated");
|
|
1127
|
+
if (this.isInitiator) throw Error("already initiated");
|
|
1046
1128
|
this.isInitiator = true;
|
|
1047
|
-
let output = new WrappedBuffer;
|
|
1129
|
+
let output = new WrappedBuffer();
|
|
1048
1130
|
output.extend([PROTOCOL_VERSION]);
|
|
1049
1131
|
await this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output);
|
|
1050
1132
|
return this._renderOutput(output);
|
|
@@ -1056,23 +1138,24 @@ class Negentropy {
|
|
|
1056
1138
|
let haveIds = [], needIds = [];
|
|
1057
1139
|
query = new WrappedBuffer(loadInputBuffer(query));
|
|
1058
1140
|
this.lastTimestampIn = this.lastTimestampOut = 0;
|
|
1059
|
-
let fullOutput = new WrappedBuffer;
|
|
1141
|
+
let fullOutput = new WrappedBuffer();
|
|
1060
1142
|
fullOutput.extend([PROTOCOL_VERSION]);
|
|
1061
1143
|
let protocolVersion = getByte(query);
|
|
1062
1144
|
if (protocolVersion < 96 || protocolVersion > 111)
|
|
1063
1145
|
throw Error("invalid negentropy protocol version byte");
|
|
1064
1146
|
if (protocolVersion !== PROTOCOL_VERSION) {
|
|
1065
1147
|
if (this.isInitiator)
|
|
1066
|
-
throw Error(
|
|
1067
|
-
|
|
1068
|
-
|
|
1148
|
+
throw Error(
|
|
1149
|
+
"unsupported negentropy protocol version requested: " + (protocolVersion - 96)
|
|
1150
|
+
);
|
|
1151
|
+
else return [this._renderOutput(fullOutput), haveIds, needIds];
|
|
1069
1152
|
}
|
|
1070
1153
|
let storageSize = this.storage.size();
|
|
1071
1154
|
let prevBound = this._bound(0);
|
|
1072
1155
|
let prevIndex = 0;
|
|
1073
1156
|
let skip = false;
|
|
1074
1157
|
while (query.length !== 0) {
|
|
1075
|
-
let o = new WrappedBuffer;
|
|
1158
|
+
let o = new WrappedBuffer();
|
|
1076
1159
|
let doSkip = () => {
|
|
1077
1160
|
if (skip) {
|
|
1078
1161
|
skip = false;
|
|
@@ -1098,10 +1181,9 @@ class Negentropy {
|
|
|
1098
1181
|
} else if (mode === Mode.IdList) {
|
|
1099
1182
|
let numIds = decodeVarInt(query);
|
|
1100
1183
|
let theirElems = {};
|
|
1101
|
-
for (let i = 0;i < numIds; i++) {
|
|
1184
|
+
for (let i = 0; i < numIds; i++) {
|
|
1102
1185
|
let e = getBytes(query, ID_SIZE);
|
|
1103
|
-
if (this.isInitiator)
|
|
1104
|
-
theirElems[e] = e;
|
|
1186
|
+
if (this.isInitiator) theirElems[e] = e;
|
|
1105
1187
|
}
|
|
1106
1188
|
if (this.isInitiator) {
|
|
1107
1189
|
skip = true;
|
|
@@ -1120,7 +1202,7 @@ class Negentropy {
|
|
|
1120
1202
|
}
|
|
1121
1203
|
} else {
|
|
1122
1204
|
doSkip();
|
|
1123
|
-
let responseIds = new WrappedBuffer;
|
|
1205
|
+
let responseIds = new WrappedBuffer();
|
|
1124
1206
|
let numResponseIds = 0;
|
|
1125
1207
|
let endBound = currBound;
|
|
1126
1208
|
this.storage.iterate(lower, upper, (item, index) => {
|
|
@@ -1138,7 +1220,7 @@ class Negentropy {
|
|
|
1138
1220
|
o.extend(encodeVarInt(numResponseIds));
|
|
1139
1221
|
o.extend(responseIds);
|
|
1140
1222
|
fullOutput.extend(o);
|
|
1141
|
-
o = new WrappedBuffer;
|
|
1223
|
+
o = new WrappedBuffer();
|
|
1142
1224
|
}
|
|
1143
1225
|
} else {
|
|
1144
1226
|
throw Error("unexpected mode");
|
|
@@ -1176,7 +1258,7 @@ class Negentropy {
|
|
|
1176
1258
|
let itemsPerBucket = Math.floor(numElems / buckets);
|
|
1177
1259
|
let bucketsWithExtra = numElems % buckets;
|
|
1178
1260
|
let curr = lower;
|
|
1179
|
-
for (let i = 0;i < buckets; i++) {
|
|
1261
|
+
for (let i = 0; i < buckets; i++) {
|
|
1180
1262
|
let bucketSize = itemsPerBucket + (i < bucketsWithExtra ? 1 : 0);
|
|
1181
1263
|
let ourFingerprint = await this.storage.fingerprint(curr, curr + bucketSize);
|
|
1182
1264
|
curr += bucketSize;
|
|
@@ -1186,10 +1268,8 @@ class Negentropy {
|
|
|
1186
1268
|
} else {
|
|
1187
1269
|
let prevItem, currItem;
|
|
1188
1270
|
this.storage.iterate(curr - 1, curr + 1, (item, index) => {
|
|
1189
|
-
if (index === curr - 1)
|
|
1190
|
-
|
|
1191
|
-
else
|
|
1192
|
-
currItem = item;
|
|
1271
|
+
if (index === curr - 1) prevItem = item;
|
|
1272
|
+
else currItem = item;
|
|
1193
1273
|
return true;
|
|
1194
1274
|
});
|
|
1195
1275
|
nextBound = this.getMinimalBound(prevItem, currItem);
|
|
@@ -1202,13 +1282,13 @@ class Negentropy {
|
|
|
1202
1282
|
}
|
|
1203
1283
|
_renderOutput(o) {
|
|
1204
1284
|
o = o.unwrap();
|
|
1205
|
-
if (!this.wantUint8ArrayOutput)
|
|
1206
|
-
o = uint8ArrayToHex(o);
|
|
1285
|
+
if (!this.wantUint8ArrayOutput) o = uint8ArrayToHex(o);
|
|
1207
1286
|
return o;
|
|
1208
1287
|
}
|
|
1209
1288
|
exceededFrameSizeLimit(n) {
|
|
1210
1289
|
return this.frameSizeLimit && n > this.frameSizeLimit - 200;
|
|
1211
1290
|
}
|
|
1291
|
+
// Decoding
|
|
1212
1292
|
decodeTimestampIn(encoded) {
|
|
1213
1293
|
let timestamp = decodeVarInt(encoded);
|
|
1214
1294
|
timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1;
|
|
@@ -1223,11 +1303,11 @@ class Negentropy {
|
|
|
1223
1303
|
decodeBound(encoded) {
|
|
1224
1304
|
let timestamp = this.decodeTimestampIn(encoded);
|
|
1225
1305
|
let len = decodeVarInt(encoded);
|
|
1226
|
-
if (len > ID_SIZE)
|
|
1227
|
-
throw Error("bound key too long");
|
|
1306
|
+
if (len > ID_SIZE) throw Error("bound key too long");
|
|
1228
1307
|
let id = getBytes(encoded, len);
|
|
1229
1308
|
return { timestamp, id };
|
|
1230
1309
|
}
|
|
1310
|
+
// Encoding
|
|
1231
1311
|
encodeTimestampOut(timestamp) {
|
|
1232
1312
|
if (timestamp === Number.MAX_VALUE) {
|
|
1233
1313
|
this.lastTimestampOut = Number.MAX_VALUE;
|
|
@@ -1239,7 +1319,7 @@ class Negentropy {
|
|
|
1239
1319
|
return encodeVarInt(timestamp + 1);
|
|
1240
1320
|
}
|
|
1241
1321
|
encodeBound(key) {
|
|
1242
|
-
let output = new WrappedBuffer;
|
|
1322
|
+
let output = new WrappedBuffer();
|
|
1243
1323
|
output.extend(this.encodeTimestampOut(key.timestamp));
|
|
1244
1324
|
output.extend(encodeVarInt(key.id.length));
|
|
1245
1325
|
output.extend(key.id);
|
|
@@ -1252,30 +1332,24 @@ class Negentropy {
|
|
|
1252
1332
|
let sharedPrefixBytes = 0;
|
|
1253
1333
|
let currKey = curr.id;
|
|
1254
1334
|
let prevKey = prev.id;
|
|
1255
|
-
for (let i = 0;i < ID_SIZE; i++) {
|
|
1256
|
-
if (currKey[i] !== prevKey[i])
|
|
1257
|
-
break;
|
|
1335
|
+
for (let i = 0; i < ID_SIZE; i++) {
|
|
1336
|
+
if (currKey[i] !== prevKey[i]) break;
|
|
1258
1337
|
sharedPrefixBytes++;
|
|
1259
1338
|
}
|
|
1260
1339
|
return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1));
|
|
1261
1340
|
}
|
|
1262
1341
|
}
|
|
1263
|
-
}
|
|
1342
|
+
};
|
|
1264
1343
|
function loadInputBuffer(inp) {
|
|
1265
|
-
if (typeof inp === "string")
|
|
1266
|
-
|
|
1267
|
-
else if (__proto__ !== Uint8Array.prototype)
|
|
1268
|
-
inp = new Uint8Array(inp);
|
|
1344
|
+
if (typeof inp === "string") inp = hexToUint8Array(inp);
|
|
1345
|
+
else if (__proto__ !== Uint8Array.prototype) inp = new Uint8Array(inp);
|
|
1269
1346
|
return inp;
|
|
1270
1347
|
}
|
|
1271
1348
|
function hexToUint8Array(h) {
|
|
1272
|
-
if (h.startsWith("0x"))
|
|
1273
|
-
|
|
1274
|
-
if (h.length % 2 === 1)
|
|
1275
|
-
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");
|
|
1276
1351
|
let arr = new Uint8Array(h.length / 2);
|
|
1277
|
-
for (let i = 0;i < arr.length; i++)
|
|
1278
|
-
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);
|
|
1279
1353
|
return arr;
|
|
1280
1354
|
}
|
|
1281
1355
|
var uint8ArrayToHexLookupTable = new Array(256);
|
|
@@ -1298,28 +1372,24 @@ var uint8ArrayToHexLookupTable = new Array(256);
|
|
|
1298
1372
|
"e",
|
|
1299
1373
|
"f"
|
|
1300
1374
|
];
|
|
1301
|
-
for (let i = 0;i < 256; i++) {
|
|
1375
|
+
for (let i = 0; i < 256; i++) {
|
|
1302
1376
|
uint8ArrayToHexLookupTable[i] = hexAlphabet[i >>> 4 & 15] + hexAlphabet[i & 15];
|
|
1303
1377
|
}
|
|
1304
1378
|
}
|
|
1305
1379
|
function uint8ArrayToHex(arr) {
|
|
1306
1380
|
let out = "";
|
|
1307
|
-
for (let i = 0, edx = arr.length;i < edx; i++) {
|
|
1381
|
+
for (let i = 0, edx = arr.length; i < edx; i++) {
|
|
1308
1382
|
out += uint8ArrayToHexLookupTable[arr[i]];
|
|
1309
1383
|
}
|
|
1310
1384
|
return out;
|
|
1311
1385
|
}
|
|
1312
1386
|
function compareUint8Array(a, b) {
|
|
1313
|
-
for (let i = 0;i < a.byteLength; i++) {
|
|
1314
|
-
if (a[i] < b[i])
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
if (a.byteLength > b.byteLength)
|
|
1320
|
-
return 1;
|
|
1321
|
-
if (a.byteLength < b.byteLength)
|
|
1322
|
-
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;
|
|
1323
1393
|
return 0;
|
|
1324
1394
|
}
|
|
1325
1395
|
function itemCompare(a, b) {
|
|
@@ -1335,7 +1405,7 @@ import { GiftWrap } from "nostr-tools/kinds";
|
|
|
1335
1405
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1336
1406
|
return Effect7.gen(function* () {
|
|
1337
1407
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1338
|
-
const storageVector = new NegentropyStorageVector;
|
|
1408
|
+
const storageVector = new NegentropyStorageVector();
|
|
1339
1409
|
for (const gw of allGiftWraps) {
|
|
1340
1410
|
storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
|
|
1341
1411
|
}
|
|
@@ -1359,8 +1429,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1359
1429
|
let currentMsg = initialMsg;
|
|
1360
1430
|
while (currentMsg !== null) {
|
|
1361
1431
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1362
|
-
if (response.msgHex === null)
|
|
1363
|
-
break;
|
|
1432
|
+
if (response.msgHex === null) break;
|
|
1364
1433
|
const reconcileResult = yield* Effect7.try({
|
|
1365
1434
|
try: () => neg.reconcile(response.msgHex),
|
|
1366
1435
|
catch: (e) => new SyncError({
|
|
@@ -1370,14 +1439,17 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1370
1439
|
})
|
|
1371
1440
|
});
|
|
1372
1441
|
const [nextMsg, haveIds, needIds] = reconcileResult;
|
|
1373
|
-
for (const id of haveIds)
|
|
1374
|
-
|
|
1375
|
-
for (const id of needIds)
|
|
1376
|
-
allNeedIds.push(id);
|
|
1442
|
+
for (const id of haveIds) allHaveIds.push(id);
|
|
1443
|
+
for (const id of needIds) allNeedIds.push(id);
|
|
1377
1444
|
currentMsg = nextMsg;
|
|
1378
1445
|
}
|
|
1446
|
+
yield* Effect7.logDebug("Negentropy reconciliation complete", {
|
|
1447
|
+
relay: relayUrl,
|
|
1448
|
+
have: allHaveIds.length,
|
|
1449
|
+
need: allNeedIds.length
|
|
1450
|
+
});
|
|
1379
1451
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1380
|
-
});
|
|
1452
|
+
}).pipe(Effect7.withLogSpan("tablinum.negentropy"));
|
|
1381
1453
|
}
|
|
1382
1454
|
|
|
1383
1455
|
// src/db/key-rotation.ts
|
|
@@ -1416,12 +1488,11 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1416
1488
|
kind: 1,
|
|
1417
1489
|
content: JSON.stringify(rotationData),
|
|
1418
1490
|
tags: [["d", `_system:rotation:${epochId}`]],
|
|
1419
|
-
created_at: Math.floor(Date.now() /
|
|
1491
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1420
1492
|
};
|
|
1421
1493
|
const wrappedEvents = [];
|
|
1422
1494
|
for (const memberPubkey of remainingMemberPubkeys) {
|
|
1423
|
-
if (memberPubkey === senderPublicKey)
|
|
1424
|
-
continue;
|
|
1495
|
+
if (memberPubkey === senderPublicKey) continue;
|
|
1425
1496
|
const wrapped = wrapEvent(rumor, senderPrivateKey, memberPubkey);
|
|
1426
1497
|
wrappedEvents.push(wrapped);
|
|
1427
1498
|
}
|
|
@@ -1434,7 +1505,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1434
1505
|
kind: 1,
|
|
1435
1506
|
content: JSON.stringify(removalData),
|
|
1436
1507
|
tags: [["d", `_system:removed:${epochId}`]],
|
|
1437
|
-
created_at: Math.floor(Date.now() /
|
|
1508
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1438
1509
|
};
|
|
1439
1510
|
const removalNotices = [];
|
|
1440
1511
|
for (const removedPubkey of removedMemberPubkeys) {
|
|
@@ -1444,8 +1515,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
|
|
|
1444
1515
|
return { epoch, wrappedEvents, removalNotices };
|
|
1445
1516
|
}
|
|
1446
1517
|
function parseRotationEvent(content, dTag) {
|
|
1447
|
-
if (!dTag.startsWith("_system:rotation:"))
|
|
1448
|
-
return Option4.none();
|
|
1518
|
+
if (!dTag.startsWith("_system:rotation:")) return Option4.none();
|
|
1449
1519
|
try {
|
|
1450
1520
|
return Option4.some(decodeRotationData(content));
|
|
1451
1521
|
} catch {
|
|
@@ -1453,8 +1523,7 @@ function parseRotationEvent(content, dTag) {
|
|
|
1453
1523
|
}
|
|
1454
1524
|
}
|
|
1455
1525
|
function parseRemovalNotice(content, dTag) {
|
|
1456
|
-
if (!dTag.startsWith("_system:removed:"))
|
|
1457
|
-
return Option4.none();
|
|
1526
|
+
if (!dTag.startsWith("_system:removed:")) return Option4.none();
|
|
1458
1527
|
try {
|
|
1459
1528
|
return Option4.some(decodeRemovalNotice(content));
|
|
1460
1529
|
} catch {
|
|
@@ -1463,7 +1532,8 @@ function parseRemovalNotice(content, dTag) {
|
|
|
1463
1532
|
}
|
|
1464
1533
|
|
|
1465
1534
|
// src/sync/sync-service.ts
|
|
1466
|
-
function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
|
|
1535
|
+
function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, logLevel, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
|
|
1536
|
+
const logLayer = Layer.succeed(References2.MinimumLogLevel, logLevel);
|
|
1467
1537
|
const getSubscriptionPubKeys = () => {
|
|
1468
1538
|
return getAllPublicKeys(epochStore);
|
|
1469
1539
|
};
|
|
@@ -1473,64 +1543,79 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1473
1543
|
kind: "create"
|
|
1474
1544
|
});
|
|
1475
1545
|
const forkHandled = (effect) => {
|
|
1476
|
-
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
|
+
);
|
|
1477
1554
|
};
|
|
1478
1555
|
let autoFlushActive = false;
|
|
1479
1556
|
const autoFlushEffect = Effect8.gen(function* () {
|
|
1480
1557
|
const size = yield* publishQueue.size();
|
|
1481
|
-
if (size === 0)
|
|
1482
|
-
return;
|
|
1558
|
+
if (size === 0) return;
|
|
1483
1559
|
yield* syncStatus.set("syncing");
|
|
1484
1560
|
yield* publishQueue.flush(relayUrls);
|
|
1485
1561
|
const remaining = yield* publishQueue.size();
|
|
1486
|
-
if (remaining > 0)
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
+
);
|
|
1489
1568
|
const scheduleAutoFlush = () => {
|
|
1490
|
-
if (autoFlushActive)
|
|
1491
|
-
return;
|
|
1569
|
+
if (autoFlushActive) return;
|
|
1492
1570
|
autoFlushActive = true;
|
|
1493
|
-
forkHandled(
|
|
1494
|
-
|
|
1495
|
-
|
|
1571
|
+
forkHandled(
|
|
1572
|
+
autoFlushEffect.pipe(
|
|
1573
|
+
Effect8.ensuring(
|
|
1574
|
+
Effect8.sync(() => {
|
|
1575
|
+
autoFlushActive = false;
|
|
1576
|
+
})
|
|
1577
|
+
)
|
|
1578
|
+
)
|
|
1579
|
+
);
|
|
1496
1580
|
};
|
|
1497
1581
|
const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
|
|
1498
1582
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1499
|
-
if (!memberRecord)
|
|
1500
|
-
return false;
|
|
1583
|
+
if (!memberRecord) return false;
|
|
1501
1584
|
return !!memberRecord.removedAt;
|
|
1502
1585
|
});
|
|
1503
1586
|
const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1504
1587
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1505
|
-
if (existing)
|
|
1506
|
-
return null;
|
|
1588
|
+
if (existing) return null;
|
|
1507
1589
|
const unwrapResult = yield* Effect8.result(giftWrapHandle.unwrap(remoteGw));
|
|
1508
1590
|
if (unwrapResult._tag === "Failure") {
|
|
1509
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1591
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1510
1592
|
return null;
|
|
1511
1593
|
}
|
|
1512
1594
|
const rumor = unwrapResult.success;
|
|
1513
1595
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1514
1596
|
if (!dTag) {
|
|
1515
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1597
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1516
1598
|
return null;
|
|
1517
1599
|
}
|
|
1518
1600
|
const colonIdx = dTag.indexOf(":");
|
|
1519
1601
|
if (colonIdx === -1) {
|
|
1520
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1602
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1521
1603
|
return null;
|
|
1522
1604
|
}
|
|
1523
1605
|
const collectionName = dTag.substring(0, colonIdx);
|
|
1524
1606
|
const recordId = dTag.substring(colonIdx + 1);
|
|
1525
1607
|
const retention = knownCollections.get(collectionName);
|
|
1526
|
-
if (retention ===
|
|
1527
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1608
|
+
if (retention === void 0) {
|
|
1609
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1528
1610
|
return null;
|
|
1529
1611
|
}
|
|
1530
1612
|
if (rumor.pubkey) {
|
|
1531
1613
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1532
1614
|
if (reject) {
|
|
1533
|
-
yield*
|
|
1615
|
+
yield* Effect8.logWarning("Rejected write from removed member", {
|
|
1616
|
+
author: rumor.pubkey.slice(0, 12)
|
|
1617
|
+
});
|
|
1618
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1534
1619
|
return null;
|
|
1535
1620
|
}
|
|
1536
1621
|
}
|
|
@@ -1538,14 +1623,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1538
1623
|
let kind = "u";
|
|
1539
1624
|
const parsed = yield* Effect8.try({
|
|
1540
1625
|
try: () => JSON.parse(rumor.content),
|
|
1541
|
-
catch: () =>
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
return;
|
|
1546
|
-
}));
|
|
1547
|
-
if (parsed === undefined) {
|
|
1548
|
-
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1626
|
+
catch: () => void 0
|
|
1627
|
+
}).pipe(Effect8.orElseSucceed(() => void 0));
|
|
1628
|
+
if (parsed === void 0) {
|
|
1629
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
|
|
1549
1630
|
return null;
|
|
1550
1631
|
}
|
|
1551
1632
|
if (parsed === null || parsed._deleted) {
|
|
@@ -1553,18 +1634,19 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1553
1634
|
} else {
|
|
1554
1635
|
data = parsed;
|
|
1555
1636
|
}
|
|
1556
|
-
const author = rumor.pubkey ||
|
|
1637
|
+
const author = rumor.pubkey || void 0;
|
|
1557
1638
|
const event = {
|
|
1558
1639
|
id: rumor.id,
|
|
1559
1640
|
collection: collectionName,
|
|
1560
1641
|
recordId,
|
|
1561
1642
|
kind,
|
|
1562
1643
|
data,
|
|
1563
|
-
createdAt: rumor.created_at *
|
|
1644
|
+
createdAt: rumor.created_at * 1e3,
|
|
1564
1645
|
author
|
|
1565
1646
|
};
|
|
1566
1647
|
yield* storage.putGiftWrap({
|
|
1567
1648
|
id: remoteGw.id,
|
|
1649
|
+
event: remoteGw,
|
|
1568
1650
|
createdAt: remoteGw.created_at
|
|
1569
1651
|
});
|
|
1570
1652
|
yield* storage.putEvent(event);
|
|
@@ -1572,6 +1654,12 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1572
1654
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1573
1655
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1574
1656
|
}
|
|
1657
|
+
yield* Effect8.logDebug("Processed gift wrap", {
|
|
1658
|
+
collection: collectionName,
|
|
1659
|
+
recordId,
|
|
1660
|
+
kind,
|
|
1661
|
+
author: author?.slice(0, 12)
|
|
1662
|
+
});
|
|
1575
1663
|
if (author && onNewAuthor) {
|
|
1576
1664
|
onNewAuthor(author);
|
|
1577
1665
|
}
|
|
@@ -1584,32 +1672,34 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1584
1672
|
}
|
|
1585
1673
|
});
|
|
1586
1674
|
const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1587
|
-
const unwrapResult = yield* Effect8.result(
|
|
1588
|
-
try
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1675
|
+
const unwrapResult = yield* Effect8.result(
|
|
1676
|
+
Effect8.try({
|
|
1677
|
+
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1678
|
+
catch: (e) => new CryptoError({
|
|
1679
|
+
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
1680
|
+
cause: e
|
|
1681
|
+
})
|
|
1592
1682
|
})
|
|
1593
|
-
|
|
1594
|
-
if (unwrapResult._tag === "Failure")
|
|
1595
|
-
return false;
|
|
1683
|
+
);
|
|
1684
|
+
if (unwrapResult._tag === "Failure") return false;
|
|
1596
1685
|
const rumor = unwrapResult.success;
|
|
1597
1686
|
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1598
|
-
if (!dTag)
|
|
1599
|
-
return false;
|
|
1687
|
+
if (!dTag) return false;
|
|
1600
1688
|
const removalNoticeOpt = parseRemovalNotice(rumor.content, dTag);
|
|
1601
1689
|
if (Option5.isSome(removalNoticeOpt)) {
|
|
1602
|
-
if (onRemoved)
|
|
1603
|
-
onRemoved(removalNoticeOpt.value);
|
|
1690
|
+
if (onRemoved) onRemoved(removalNoticeOpt.value);
|
|
1604
1691
|
return true;
|
|
1605
1692
|
}
|
|
1606
1693
|
const rotationDataOpt = parseRotationEvent(rumor.content, dTag);
|
|
1607
|
-
if (Option5.isNone(rotationDataOpt))
|
|
1608
|
-
return false;
|
|
1694
|
+
if (Option5.isNone(rotationDataOpt)) return false;
|
|
1609
1695
|
const rotationData = rotationDataOpt.value;
|
|
1610
|
-
if (epochStore.epochs.has(rotationData.epochId))
|
|
1611
|
-
|
|
1612
|
-
|
|
1696
|
+
if (epochStore.epochs.has(rotationData.epochId)) return false;
|
|
1697
|
+
const epoch = createEpochKey(
|
|
1698
|
+
rotationData.epochId,
|
|
1699
|
+
rotationData.epochKey,
|
|
1700
|
+
rumor.pubkey || "",
|
|
1701
|
+
rotationData.parentEpoch
|
|
1702
|
+
);
|
|
1613
1703
|
addEpoch(epochStore, epoch);
|
|
1614
1704
|
epochStore.currentEpochId = epoch.id;
|
|
1615
1705
|
let membersChanged = false;
|
|
@@ -1629,75 +1719,158 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1629
1719
|
membersChanged = true;
|
|
1630
1720
|
}
|
|
1631
1721
|
}
|
|
1632
|
-
if (membersChanged && onMembersChanged)
|
|
1633
|
-
onMembersChanged();
|
|
1722
|
+
if (membersChanged && onMembersChanged) onMembersChanged();
|
|
1634
1723
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1635
1724
|
return true;
|
|
1636
1725
|
});
|
|
1637
|
-
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1726
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
|
|
1727
|
+
relayUrls,
|
|
1728
|
+
(url) => Effect8.gen(function* () {
|
|
1729
|
+
yield* relay.subscribe(filter, url, (event) => {
|
|
1730
|
+
forkHandled(onEvent(event));
|
|
1731
|
+
}).pipe(
|
|
1732
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1733
|
+
Effect8.ignore
|
|
1734
|
+
);
|
|
1735
|
+
}),
|
|
1736
|
+
{ discard: true }
|
|
1737
|
+
);
|
|
1642
1738
|
const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1643
|
-
|
|
1739
|
+
yield* Effect8.logDebug("Syncing relay", { relay: url });
|
|
1740
|
+
const reconcileResult = yield* Effect8.result(
|
|
1741
|
+
reconcileWithRelay(storage, relay, url, Array.from(pubKeys))
|
|
1742
|
+
);
|
|
1644
1743
|
if (reconcileResult._tag === "Failure") {
|
|
1645
1744
|
onSyncError?.(reconcileResult.failure);
|
|
1646
1745
|
return;
|
|
1647
1746
|
}
|
|
1648
1747
|
const { haveIds, needIds } = reconcileResult.success;
|
|
1748
|
+
yield* Effect8.logDebug("Relay reconciliation result", {
|
|
1749
|
+
relay: url,
|
|
1750
|
+
need: needIds.length,
|
|
1751
|
+
have: haveIds.length
|
|
1752
|
+
});
|
|
1649
1753
|
if (needIds.length > 0) {
|
|
1650
|
-
const fetched = yield* relay.fetchEvents(needIds, url).pipe(
|
|
1754
|
+
const fetched = yield* relay.fetchEvents(needIds, url).pipe(
|
|
1755
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1756
|
+
Effect8.orElseSucceed(() => [])
|
|
1757
|
+
);
|
|
1651
1758
|
const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
|
|
1652
|
-
yield* Effect8.forEach(
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1759
|
+
yield* Effect8.forEach(
|
|
1760
|
+
sorted,
|
|
1761
|
+
(remoteGw) => Effect8.gen(function* () {
|
|
1762
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(
|
|
1763
|
+
Effect8.orElseSucceed(() => null)
|
|
1764
|
+
);
|
|
1765
|
+
if (collection2) changedCollections.add(collection2);
|
|
1766
|
+
}),
|
|
1767
|
+
{ discard: true }
|
|
1768
|
+
);
|
|
1657
1769
|
}
|
|
1658
1770
|
if (haveIds.length > 0) {
|
|
1659
|
-
yield* Effect8.forEach(
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1771
|
+
yield* Effect8.forEach(
|
|
1772
|
+
haveIds,
|
|
1773
|
+
(id) => Effect8.gen(function* () {
|
|
1774
|
+
const gw = yield* storage.getGiftWrap(id);
|
|
1775
|
+
if (!gw?.event) return;
|
|
1776
|
+
yield* relay.publish(gw.event, [url]).pipe(
|
|
1777
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1778
|
+
Effect8.ignore
|
|
1779
|
+
);
|
|
1780
|
+
}),
|
|
1781
|
+
{ discard: true }
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
}).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
|
|
1785
|
+
let healingActive = false;
|
|
1786
|
+
const healingEffect = Effect8.gen(function* () {
|
|
1787
|
+
if (!healingActive) return;
|
|
1788
|
+
const status = yield* syncStatus.get();
|
|
1789
|
+
if (status === "syncing") return;
|
|
1790
|
+
yield* syncStatus.set("syncing");
|
|
1791
|
+
yield* Effect8.gen(function* () {
|
|
1792
|
+
const pubKeys = getSubscriptionPubKeys();
|
|
1793
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1794
|
+
yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
|
|
1795
|
+
discard: true
|
|
1796
|
+
});
|
|
1797
|
+
if (changedCollections.size > 0) {
|
|
1798
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1799
|
+
}
|
|
1800
|
+
}).pipe(Effect8.ensuring(syncStatus.set("idle")));
|
|
1801
|
+
}).pipe(Effect8.ignore);
|
|
1667
1802
|
const handle = {
|
|
1668
1803
|
sync: () => Effect8.gen(function* () {
|
|
1804
|
+
yield* Effect8.logInfo("Sync started");
|
|
1669
1805
|
yield* syncStatus.set("syncing");
|
|
1670
1806
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1671
|
-
const changedCollections = new Set;
|
|
1807
|
+
const changedCollections = /* @__PURE__ */ new Set();
|
|
1672
1808
|
yield* Effect8.gen(function* () {
|
|
1673
1809
|
const pubKeys = getSubscriptionPubKeys();
|
|
1674
1810
|
yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
|
|
1675
1811
|
discard: true
|
|
1676
1812
|
});
|
|
1677
1813
|
yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
|
|
1678
|
-
}).pipe(
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1814
|
+
}).pipe(
|
|
1815
|
+
Effect8.ensuring(
|
|
1816
|
+
Effect8.gen(function* () {
|
|
1817
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1818
|
+
yield* syncStatus.set("idle");
|
|
1819
|
+
})
|
|
1820
|
+
)
|
|
1821
|
+
);
|
|
1822
|
+
yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
1823
|
+
}).pipe(Effect8.withLogSpan("tablinum.sync")),
|
|
1683
1824
|
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1684
|
-
if (!giftWrap.event)
|
|
1685
|
-
|
|
1686
|
-
|
|
1825
|
+
if (!giftWrap.event) return;
|
|
1826
|
+
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1827
|
+
Effect8.tapError(
|
|
1828
|
+
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1829
|
+
Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
1830
|
+
Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
|
|
1831
|
+
)
|
|
1832
|
+
),
|
|
1833
|
+
Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
|
|
1834
|
+
Effect8.ignore
|
|
1835
|
+
);
|
|
1687
1836
|
}),
|
|
1688
1837
|
startSubscription: () => Effect8.gen(function* () {
|
|
1689
1838
|
const pubKeys = getSubscriptionPubKeys();
|
|
1690
1839
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1691
1840
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1692
|
-
yield* subscribeAcrossRelays(
|
|
1841
|
+
yield* subscribeAcrossRelays(
|
|
1842
|
+
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1843
|
+
(event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
|
|
1844
|
+
);
|
|
1693
1845
|
}
|
|
1694
1846
|
}),
|
|
1695
|
-
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap)
|
|
1847
|
+
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap),
|
|
1848
|
+
startHealing: () => {
|
|
1849
|
+
if (healingActive) return;
|
|
1850
|
+
healingActive = true;
|
|
1851
|
+
forkHandled(
|
|
1852
|
+
Effect8.sleep(Duration.minutes(5)).pipe(
|
|
1853
|
+
Effect8.andThen(healingEffect),
|
|
1854
|
+
Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
1855
|
+
Effect8.ensuring(Effect8.sync(() => {
|
|
1856
|
+
healingActive = false;
|
|
1857
|
+
}))
|
|
1858
|
+
)
|
|
1859
|
+
);
|
|
1860
|
+
},
|
|
1861
|
+
stopHealing: () => {
|
|
1862
|
+
healingActive = false;
|
|
1863
|
+
}
|
|
1696
1864
|
};
|
|
1697
|
-
forkHandled(
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1865
|
+
forkHandled(
|
|
1866
|
+
publishQueue.size().pipe(
|
|
1867
|
+
Effect8.flatMap(
|
|
1868
|
+
(size) => Effect8.sync(() => {
|
|
1869
|
+
if (size > 0) scheduleAutoFlush();
|
|
1870
|
+
})
|
|
1871
|
+
)
|
|
1872
|
+
)
|
|
1873
|
+
);
|
|
1701
1874
|
return handle;
|
|
1702
1875
|
}
|
|
1703
1876
|
|
|
@@ -1753,9 +1926,14 @@ var decodeAuthorProfile = Schema5.decodeUnknownEffect(Schema5.fromJsonString(Aut
|
|
|
1753
1926
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1754
1927
|
return Effect9.gen(function* () {
|
|
1755
1928
|
for (const url of relayUrls) {
|
|
1756
|
-
const result = yield* Effect9.result(
|
|
1929
|
+
const result = yield* Effect9.result(
|
|
1930
|
+
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
1931
|
+
);
|
|
1757
1932
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
1758
|
-
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1933
|
+
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1934
|
+
Effect9.map(Option6.some),
|
|
1935
|
+
Effect9.orElseSucceed(() => Option6.none())
|
|
1936
|
+
);
|
|
1759
1937
|
}
|
|
1760
1938
|
}
|
|
1761
1939
|
return Option6.none();
|
|
@@ -1764,48 +1942,47 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
|
1764
1942
|
|
|
1765
1943
|
// src/services/Identity.ts
|
|
1766
1944
|
import { ServiceMap as ServiceMap3 } from "effect";
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
}
|
|
1945
|
+
var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
|
|
1946
|
+
};
|
|
1770
1947
|
|
|
1771
1948
|
// src/services/EpochStore.ts
|
|
1772
1949
|
import { ServiceMap as ServiceMap4 } from "effect";
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1950
|
+
var EpochStore = class extends ServiceMap4.Service()(
|
|
1951
|
+
"tablinum/EpochStore"
|
|
1952
|
+
) {
|
|
1953
|
+
};
|
|
1776
1954
|
|
|
1777
1955
|
// src/services/Storage.ts
|
|
1778
1956
|
import { ServiceMap as ServiceMap5 } from "effect";
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
}
|
|
1957
|
+
var Storage = class extends ServiceMap5.Service()("tablinum/Storage") {
|
|
1958
|
+
};
|
|
1782
1959
|
|
|
1783
1960
|
// src/services/Relay.ts
|
|
1784
1961
|
import { ServiceMap as ServiceMap6 } from "effect";
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
}
|
|
1962
|
+
var Relay = class extends ServiceMap6.Service()("tablinum/Relay") {
|
|
1963
|
+
};
|
|
1788
1964
|
|
|
1789
1965
|
// src/services/GiftWrap.ts
|
|
1790
1966
|
import { ServiceMap as ServiceMap7 } from "effect";
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
}
|
|
1967
|
+
var GiftWrap3 = class extends ServiceMap7.Service()("tablinum/GiftWrap") {
|
|
1968
|
+
};
|
|
1794
1969
|
|
|
1795
1970
|
// src/services/PublishQueue.ts
|
|
1796
1971
|
import { ServiceMap as ServiceMap8 } from "effect";
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1972
|
+
var PublishQueue = class extends ServiceMap8.Service()(
|
|
1973
|
+
"tablinum/PublishQueue"
|
|
1974
|
+
) {
|
|
1975
|
+
};
|
|
1800
1976
|
|
|
1801
1977
|
// src/services/SyncStatus.ts
|
|
1802
1978
|
import { ServiceMap as ServiceMap9 } from "effect";
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1979
|
+
var SyncStatus = class extends ServiceMap9.Service()(
|
|
1980
|
+
"tablinum/SyncStatus"
|
|
1981
|
+
) {
|
|
1982
|
+
};
|
|
1806
1983
|
|
|
1807
1984
|
// src/layers/IdentityLive.ts
|
|
1808
|
-
import { Effect as Effect11, Layer } from "effect";
|
|
1985
|
+
import { Effect as Effect11, Layer as Layer2 } from "effect";
|
|
1809
1986
|
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1810
1987
|
|
|
1811
1988
|
// src/db/identity.ts
|
|
@@ -1843,47 +2020,128 @@ function createIdentity(suppliedKey) {
|
|
|
1843
2020
|
}
|
|
1844
2021
|
|
|
1845
2022
|
// src/layers/IdentityLive.ts
|
|
1846
|
-
var IdentityLive =
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2023
|
+
var IdentityLive = Layer2.effect(
|
|
2024
|
+
Identity,
|
|
2025
|
+
Effect11.gen(function* () {
|
|
2026
|
+
const config = yield* Config;
|
|
2027
|
+
const storage = yield* Storage;
|
|
2028
|
+
const idbKey = yield* storage.getMeta("identity_key");
|
|
2029
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
|
|
2030
|
+
const identity = yield* createIdentity(resolvedKey);
|
|
2031
|
+
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2032
|
+
yield* Effect11.logInfo("Identity loaded", {
|
|
2033
|
+
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2034
|
+
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2035
|
+
});
|
|
2036
|
+
return identity;
|
|
2037
|
+
})
|
|
2038
|
+
);
|
|
1855
2039
|
|
|
1856
2040
|
// src/layers/EpochStoreLive.ts
|
|
1857
|
-
import { Effect as Effect12, Layer as
|
|
2041
|
+
import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
|
|
1858
2042
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1859
2043
|
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
1860
|
-
var EpochStoreLive =
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
const
|
|
1867
|
-
if (
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2044
|
+
var EpochStoreLive = Layer3.effect(
|
|
2045
|
+
EpochStore,
|
|
2046
|
+
Effect12.gen(function* () {
|
|
2047
|
+
const config = yield* Config;
|
|
2048
|
+
const identity = yield* Identity;
|
|
2049
|
+
const storage = yield* Storage;
|
|
2050
|
+
const idbRaw = yield* storage.getMeta("epochs");
|
|
2051
|
+
if (typeof idbRaw === "string") {
|
|
2052
|
+
const idbStore = deserializeEpochStore(idbRaw);
|
|
2053
|
+
if (Option7.isSome(idbStore)) {
|
|
2054
|
+
yield* Effect12.logInfo("Epoch store loaded", {
|
|
2055
|
+
source: "storage",
|
|
2056
|
+
epochs: idbStore.value.epochs.size
|
|
2057
|
+
});
|
|
2058
|
+
return idbStore.value;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
2062
|
+
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2063
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2064
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2065
|
+
return store2;
|
|
2066
|
+
}
|
|
2067
|
+
const store = createEpochStoreFromInputs(
|
|
2068
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
|
|
2069
|
+
{ createdBy: identity.publicKey }
|
|
2070
|
+
);
|
|
2071
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2072
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2073
|
+
return store;
|
|
2074
|
+
})
|
|
2075
|
+
);
|
|
1880
2076
|
|
|
1881
2077
|
// src/layers/StorageLive.ts
|
|
1882
|
-
import { Effect as Effect14, Layer as
|
|
2078
|
+
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
1883
2079
|
|
|
1884
2080
|
// src/storage/idb.ts
|
|
1885
2081
|
import { Effect as Effect13 } from "effect";
|
|
1886
2082
|
import { openDB } from "idb";
|
|
2083
|
+
|
|
2084
|
+
// src/sync/compact-event.ts
|
|
2085
|
+
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2086
|
+
var VERSION = 1;
|
|
2087
|
+
var HEADER_SIZE = 133;
|
|
2088
|
+
function base64ToBytes(base64) {
|
|
2089
|
+
const binary = atob(base64);
|
|
2090
|
+
const bytes = new Uint8Array(binary.length);
|
|
2091
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2092
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2093
|
+
}
|
|
2094
|
+
return bytes;
|
|
2095
|
+
}
|
|
2096
|
+
function bytesToBase64(bytes) {
|
|
2097
|
+
let binary = "";
|
|
2098
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2099
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2100
|
+
}
|
|
2101
|
+
return btoa(binary);
|
|
2102
|
+
}
|
|
2103
|
+
function packEvent(event) {
|
|
2104
|
+
const pubkey = hexToBytes4(event.pubkey);
|
|
2105
|
+
const sig = hexToBytes4(event.sig);
|
|
2106
|
+
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2107
|
+
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2108
|
+
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2109
|
+
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2110
|
+
}
|
|
2111
|
+
const recipient = hexToBytes4(recipientTag[1]);
|
|
2112
|
+
const createdAtBuf = new Uint8Array(4);
|
|
2113
|
+
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2114
|
+
const content = base64ToBytes(event.content);
|
|
2115
|
+
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2116
|
+
result[0] = VERSION;
|
|
2117
|
+
result.set(pubkey, 1);
|
|
2118
|
+
result.set(sig, 33);
|
|
2119
|
+
result.set(recipient, 97);
|
|
2120
|
+
result.set(createdAtBuf, 129);
|
|
2121
|
+
result.set(content, HEADER_SIZE);
|
|
2122
|
+
return result;
|
|
2123
|
+
}
|
|
2124
|
+
function unpackEvent(id, compact) {
|
|
2125
|
+
const version = compact[0];
|
|
2126
|
+
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2127
|
+
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2128
|
+
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2129
|
+
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2130
|
+
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2131
|
+
const createdAt = dv.getUint32(0, false);
|
|
2132
|
+
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2133
|
+
return {
|
|
2134
|
+
id,
|
|
2135
|
+
pubkey,
|
|
2136
|
+
sig,
|
|
2137
|
+
created_at: createdAt,
|
|
2138
|
+
kind: 1059,
|
|
2139
|
+
tags: [["p", recipient]],
|
|
2140
|
+
content
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/storage/idb.ts
|
|
1887
2145
|
var DB_NAME = "tablinum";
|
|
1888
2146
|
function storeName(collection2) {
|
|
1889
2147
|
return `col_${collection2}`;
|
|
@@ -1914,7 +2172,7 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1914
2172
|
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
1915
2173
|
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
1916
2174
|
}
|
|
1917
|
-
const expectedStores = new Set;
|
|
2175
|
+
const expectedStores = /* @__PURE__ */ new Set();
|
|
1918
2176
|
for (const [, def] of Object.entries(schema)) {
|
|
1919
2177
|
const sn = storeName(def.name);
|
|
1920
2178
|
expectedStores.add(sn);
|
|
@@ -1928,12 +2186,10 @@ function upgradeSchema(database, schema, tx) {
|
|
|
1928
2186
|
const existingIndices = new Set(Array.from(store.indexNames));
|
|
1929
2187
|
const wantedIndices = new Set(def.indices ?? []);
|
|
1930
2188
|
for (const idx of existingIndices) {
|
|
1931
|
-
if (!wantedIndices.has(idx))
|
|
1932
|
-
store.deleteIndex(idx);
|
|
2189
|
+
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
1933
2190
|
}
|
|
1934
2191
|
for (const idx of wantedIndices) {
|
|
1935
|
-
if (!existingIndices.has(idx))
|
|
1936
|
-
store.createIndex(idx, idx);
|
|
2192
|
+
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
1937
2193
|
}
|
|
1938
2194
|
}
|
|
1939
2195
|
}
|
|
@@ -1948,6 +2204,13 @@ function openIDBStorage(dbName, schema) {
|
|
|
1948
2204
|
return Effect13.gen(function* () {
|
|
1949
2205
|
const name = dbName ?? DB_NAME;
|
|
1950
2206
|
const schemaSig = computeSchemaSig(schema);
|
|
2207
|
+
if (typeof indexedDB === "undefined") {
|
|
2208
|
+
return yield* Effect13.fail(
|
|
2209
|
+
new StorageError({
|
|
2210
|
+
message: "IndexedDB is not available in this environment"
|
|
2211
|
+
})
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
1951
2214
|
const probeDb = yield* Effect13.tryPromise({
|
|
1952
2215
|
try: () => openDB(name),
|
|
1953
2216
|
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
@@ -1958,7 +2221,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
1958
2221
|
const storedSig = yield* Effect13.tryPromise({
|
|
1959
2222
|
try: () => probeDb.get("_meta", "schema_sig"),
|
|
1960
2223
|
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
1961
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(
|
|
2224
|
+
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
1962
2225
|
needsUpgrade = storedSig !== schemaSig;
|
|
1963
2226
|
}
|
|
1964
2227
|
probeDb.close();
|
|
@@ -1975,9 +2238,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
1975
2238
|
});
|
|
1976
2239
|
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
1977
2240
|
const handle = {
|
|
1978
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() =>
|
|
1979
|
-
return;
|
|
1980
|
-
})),
|
|
2241
|
+
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
1981
2242
|
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
1982
2243
|
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
1983
2244
|
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
@@ -1997,29 +2258,52 @@ function openIDBStorage(dbName, schema) {
|
|
|
1997
2258
|
}
|
|
1998
2259
|
return results;
|
|
1999
2260
|
}),
|
|
2000
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() =>
|
|
2001
|
-
return;
|
|
2002
|
-
})),
|
|
2261
|
+
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2003
2262
|
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2004
2263
|
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2005
|
-
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
const existing = await db.get("giftwraps", id);
|
|
2016
|
-
if (existing) {
|
|
2017
|
-
await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
|
|
2264
|
+
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2265
|
+
"getEventsByRecord",
|
|
2266
|
+
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2267
|
+
),
|
|
2268
|
+
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2269
|
+
if (gw.event) {
|
|
2270
|
+
const compact = packEvent(gw.event);
|
|
2271
|
+
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2272
|
+
} else {
|
|
2273
|
+
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2018
2274
|
}
|
|
2019
2275
|
}),
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2276
|
+
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2277
|
+
const raw = await db.get("giftwraps", id);
|
|
2278
|
+
if (!raw) return void 0;
|
|
2279
|
+
if (raw.compact) {
|
|
2280
|
+
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2281
|
+
}
|
|
2282
|
+
if (raw.event) {
|
|
2283
|
+
const compact = packEvent(raw.event);
|
|
2284
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2285
|
+
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2286
|
+
}
|
|
2287
|
+
return { id: raw.id, createdAt: raw.createdAt };
|
|
2288
|
+
}),
|
|
2289
|
+
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2290
|
+
const raws = await db.getAll("giftwraps");
|
|
2291
|
+
const results = [];
|
|
2292
|
+
for (const raw of raws) {
|
|
2293
|
+
if (raw.compact) {
|
|
2294
|
+
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2295
|
+
} else if (raw.event) {
|
|
2296
|
+
const compact = packEvent(raw.event);
|
|
2297
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2298
|
+
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2299
|
+
} else {
|
|
2300
|
+
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return results;
|
|
2304
|
+
}),
|
|
2305
|
+
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2306
|
+
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2023
2307
|
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2024
2308
|
const existing = await db.get("events", id);
|
|
2025
2309
|
if (existing) {
|
|
@@ -2027,9 +2311,7 @@ function openIDBStorage(dbName, schema) {
|
|
|
2027
2311
|
}
|
|
2028
2312
|
}),
|
|
2029
2313
|
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2030
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() =>
|
|
2031
|
-
return;
|
|
2032
|
-
})),
|
|
2314
|
+
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2033
2315
|
close: () => Effect13.sync(() => db.close())
|
|
2034
2316
|
};
|
|
2035
2317
|
return handle;
|
|
@@ -2037,16 +2319,21 @@ function openIDBStorage(dbName, schema) {
|
|
|
2037
2319
|
}
|
|
2038
2320
|
|
|
2039
2321
|
// src/layers/StorageLive.ts
|
|
2040
|
-
var StorageLive =
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2322
|
+
var StorageLive = Layer4.effect(
|
|
2323
|
+
Storage,
|
|
2324
|
+
Effect14.gen(function* () {
|
|
2325
|
+
const config = yield* Config;
|
|
2326
|
+
const handle = yield* openIDBStorage(config.dbName, {
|
|
2327
|
+
...config.schema,
|
|
2328
|
+
_members: membersCollectionDef
|
|
2329
|
+
});
|
|
2330
|
+
yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
|
|
2331
|
+
return handle;
|
|
2332
|
+
})
|
|
2333
|
+
);
|
|
2047
2334
|
|
|
2048
2335
|
// src/layers/RelayLive.ts
|
|
2049
|
-
import { Layer as
|
|
2336
|
+
import { Layer as Layer5 } from "effect";
|
|
2050
2337
|
|
|
2051
2338
|
// src/sync/relay.ts
|
|
2052
2339
|
import { Effect as Effect15, Option as Option8, Schema as Schema6, ScopedCache, Scope as Scope3 } from "effect";
|
|
@@ -2057,32 +2344,41 @@ var NegMessageFrameSchema = Schema6.Tuple([
|
|
|
2057
2344
|
Schema6.String
|
|
2058
2345
|
]);
|
|
2059
2346
|
var NegErrorFrameSchema = Schema6.Tuple([Schema6.Literal("NEG-ERR"), Schema6.String, Schema6.String]);
|
|
2060
|
-
var decodeNegFrame = Schema6.decodeUnknownEffect(
|
|
2347
|
+
var decodeNegFrame = Schema6.decodeUnknownEffect(
|
|
2348
|
+
Schema6.fromJsonString(Schema6.Union([NegMessageFrameSchema, NegErrorFrameSchema]))
|
|
2349
|
+
);
|
|
2061
2350
|
function parseNegMessageFrame(data) {
|
|
2062
|
-
return Effect15.runSync(
|
|
2351
|
+
return Effect15.runSync(
|
|
2352
|
+
decodeNegFrame(data).pipe(
|
|
2353
|
+
Effect15.map(Option8.some),
|
|
2354
|
+
Effect15.orElseSucceed(() => Option8.none())
|
|
2355
|
+
)
|
|
2356
|
+
);
|
|
2063
2357
|
}
|
|
2064
2358
|
function createRelayHandle() {
|
|
2065
2359
|
return Effect15.gen(function* () {
|
|
2066
2360
|
const relayScope = yield* Effect15.scope;
|
|
2067
2361
|
const connections = yield* ScopedCache.make({
|
|
2068
2362
|
capacity: 64,
|
|
2069
|
-
lookup: (url) => Effect15.acquireRelease(
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2363
|
+
lookup: (url) => Effect15.acquireRelease(
|
|
2364
|
+
Effect15.tryPromise({
|
|
2365
|
+
try: () => Relay2.connect(url),
|
|
2366
|
+
catch: (e) => new RelayError({
|
|
2367
|
+
message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2368
|
+
url,
|
|
2369
|
+
cause: e
|
|
2370
|
+
})
|
|
2371
|
+
}),
|
|
2372
|
+
(relay) => Effect15.sync(() => {
|
|
2373
|
+
relay.close();
|
|
2075
2374
|
})
|
|
2076
|
-
|
|
2077
|
-
relay.close();
|
|
2078
|
-
}))
|
|
2375
|
+
)
|
|
2079
2376
|
});
|
|
2080
|
-
const connectedUrls = new Set;
|
|
2081
|
-
const statusListeners = new Set;
|
|
2377
|
+
const connectedUrls = /* @__PURE__ */ new Set();
|
|
2378
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
2082
2379
|
const notifyStatus = () => {
|
|
2083
2380
|
const status = { connectedUrls: [...connectedUrls] };
|
|
2084
|
-
for (const listener of statusListeners)
|
|
2085
|
-
listener(status);
|
|
2381
|
+
for (const listener of statusListeners) listener(status);
|
|
2086
2382
|
};
|
|
2087
2383
|
const markConnected = (url) => {
|
|
2088
2384
|
if (!connectedUrls.has(url)) {
|
|
@@ -2096,66 +2392,98 @@ function createRelayHandle() {
|
|
|
2096
2392
|
notifyStatus();
|
|
2097
2393
|
}
|
|
2098
2394
|
};
|
|
2099
|
-
const getRelay = (url) => ScopedCache.get(connections, url).pipe(
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
sub
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2395
|
+
const getRelay = (url) => ScopedCache.get(connections, url).pipe(
|
|
2396
|
+
Effect15.flatMap(
|
|
2397
|
+
(relay) => relay.connected === false ? ScopedCache.invalidate(connections, url).pipe(
|
|
2398
|
+
Effect15.andThen(ScopedCache.get(connections, url))
|
|
2399
|
+
) : Effect15.succeed(relay)
|
|
2400
|
+
)
|
|
2401
|
+
);
|
|
2402
|
+
const withRelay = (url, run) => getRelay(url).pipe(
|
|
2403
|
+
Effect15.tap(() => Effect15.sync(() => markConnected(url))),
|
|
2404
|
+
Effect15.flatMap((relay) => run(relay)),
|
|
2405
|
+
Effect15.tapError(
|
|
2406
|
+
() => ScopedCache.invalidate(connections, url).pipe(
|
|
2407
|
+
Effect15.tap(() => Effect15.sync(() => markDisconnected(url)))
|
|
2408
|
+
)
|
|
2409
|
+
)
|
|
2410
|
+
);
|
|
2411
|
+
const collectEvents = (url, filters) => withRelay(
|
|
2412
|
+
url,
|
|
2413
|
+
(relay) => Effect15.callback((resume) => {
|
|
2414
|
+
const events = [];
|
|
2415
|
+
let settled = false;
|
|
2416
|
+
let timer;
|
|
2417
|
+
let sub;
|
|
2418
|
+
const cleanup = () => {
|
|
2419
|
+
settled = true;
|
|
2420
|
+
if (timer !== void 0) {
|
|
2421
|
+
clearTimeout(timer);
|
|
2422
|
+
timer = void 0;
|
|
2423
|
+
}
|
|
2424
|
+
sub?.close();
|
|
2425
|
+
sub = void 0;
|
|
2426
|
+
};
|
|
2427
|
+
const fail = (e) => resume(
|
|
2428
|
+
Effect15.fail(
|
|
2429
|
+
new RelayError({
|
|
2430
|
+
message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2431
|
+
url,
|
|
2432
|
+
cause: e
|
|
2433
|
+
})
|
|
2434
|
+
)
|
|
2435
|
+
);
|
|
2436
|
+
try {
|
|
2437
|
+
sub = relay.subscribe([...filters], {
|
|
2438
|
+
onevent(evt) {
|
|
2439
|
+
if (!settled) {
|
|
2440
|
+
events.push(evt);
|
|
2441
|
+
}
|
|
2442
|
+
},
|
|
2443
|
+
oneose() {
|
|
2444
|
+
if (settled) return;
|
|
2445
|
+
cleanup();
|
|
2446
|
+
resume(Effect15.succeed(events));
|
|
2125
2447
|
}
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
if (settled)
|
|
2129
|
-
return;
|
|
2448
|
+
});
|
|
2449
|
+
timer = setTimeout(() => {
|
|
2450
|
+
if (settled) return;
|
|
2130
2451
|
cleanup();
|
|
2131
2452
|
resume(Effect15.succeed(events));
|
|
2132
|
-
}
|
|
2133
|
-
})
|
|
2134
|
-
timer = setTimeout(() => {
|
|
2135
|
-
if (settled)
|
|
2136
|
-
return;
|
|
2453
|
+
}, 1e4);
|
|
2454
|
+
} catch (e) {
|
|
2137
2455
|
cleanup();
|
|
2138
|
-
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
}
|
|
2144
|
-
return Effect15.sync(cleanup);
|
|
2145
|
-
}));
|
|
2456
|
+
fail(e);
|
|
2457
|
+
}
|
|
2458
|
+
return Effect15.sync(cleanup);
|
|
2459
|
+
})
|
|
2460
|
+
);
|
|
2146
2461
|
return {
|
|
2147
2462
|
publish: (event, urls) => Effect15.gen(function* () {
|
|
2148
|
-
const results = yield* Effect15.forEach(
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2463
|
+
const results = yield* Effect15.forEach(
|
|
2464
|
+
urls,
|
|
2465
|
+
(url) => Effect15.result(
|
|
2466
|
+
withRelay(
|
|
2467
|
+
url,
|
|
2468
|
+
(relay) => Effect15.tryPromise({
|
|
2469
|
+
try: () => relay.publish(event),
|
|
2470
|
+
catch: (e) => new RelayError({
|
|
2471
|
+
message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2472
|
+
url,
|
|
2473
|
+
cause: e
|
|
2474
|
+
})
|
|
2475
|
+
}).pipe(
|
|
2476
|
+
Effect15.timeoutOrElse({
|
|
2477
|
+
duration: "10 seconds",
|
|
2478
|
+
onTimeout: () => Effect15.fail(
|
|
2479
|
+
new RelayError({ message: `Publish to ${url} timed out`, url })
|
|
2480
|
+
)
|
|
2481
|
+
})
|
|
2482
|
+
)
|
|
2483
|
+
)
|
|
2484
|
+
),
|
|
2485
|
+
{ concurrency: "unbounded" }
|
|
2486
|
+
);
|
|
2159
2487
|
const failures = results.filter((r) => r._tag === "Failure");
|
|
2160
2488
|
if (failures.length === urls.length && urls.length > 0) {
|
|
2161
2489
|
return yield* new RelayError({
|
|
@@ -2164,90 +2492,104 @@ function createRelayHandle() {
|
|
|
2164
2492
|
}
|
|
2165
2493
|
}),
|
|
2166
2494
|
fetchEvents: (ids, url) => Effect15.gen(function* () {
|
|
2167
|
-
if (ids.length === 0)
|
|
2168
|
-
return [];
|
|
2495
|
+
if (ids.length === 0) return [];
|
|
2169
2496
|
return yield* collectEvents(url, [{ ids }]);
|
|
2170
2497
|
}),
|
|
2171
2498
|
fetchByFilter: (filter, url) => collectEvents(url, [filter]),
|
|
2172
|
-
subscribe: (filter, url, onEvent) => withRelay(
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
sub
|
|
2200
|
-
ws
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
return;
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
try {
|
|
2227
|
-
sub = relay.subscribe([filter], {
|
|
2228
|
-
onevent() {},
|
|
2229
|
-
oneose() {}
|
|
2230
|
-
});
|
|
2231
|
-
ws = relay._ws || relay.ws;
|
|
2232
|
-
if (!ws) {
|
|
2499
|
+
subscribe: (filter, url, onEvent) => withRelay(
|
|
2500
|
+
url,
|
|
2501
|
+
(relay) => Effect15.acquireRelease(
|
|
2502
|
+
Effect15.try({
|
|
2503
|
+
try: () => relay.subscribe([filter], {
|
|
2504
|
+
onevent(evt) {
|
|
2505
|
+
onEvent(evt);
|
|
2506
|
+
},
|
|
2507
|
+
oneose() {
|
|
2508
|
+
}
|
|
2509
|
+
}),
|
|
2510
|
+
catch: (e) => new RelayError({
|
|
2511
|
+
message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2512
|
+
url,
|
|
2513
|
+
cause: e
|
|
2514
|
+
})
|
|
2515
|
+
}),
|
|
2516
|
+
(sub) => Effect15.sync(() => {
|
|
2517
|
+
sub.close();
|
|
2518
|
+
})
|
|
2519
|
+
).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)
|
|
2520
|
+
),
|
|
2521
|
+
sendNegMsg: (url, subId, filter, msgHex) => withRelay(
|
|
2522
|
+
url,
|
|
2523
|
+
(relay) => Effect15.callback((resume) => {
|
|
2524
|
+
let settled = false;
|
|
2525
|
+
let timer;
|
|
2526
|
+
let sub;
|
|
2527
|
+
let ws;
|
|
2528
|
+
const cleanup = () => {
|
|
2529
|
+
settled = true;
|
|
2530
|
+
if (timer !== void 0) {
|
|
2531
|
+
clearTimeout(timer);
|
|
2532
|
+
timer = void 0;
|
|
2533
|
+
}
|
|
2534
|
+
sub?.close();
|
|
2535
|
+
sub = void 0;
|
|
2536
|
+
ws?.removeEventListener("message", handler);
|
|
2537
|
+
ws = void 0;
|
|
2538
|
+
};
|
|
2539
|
+
const fail = (e) => resume(
|
|
2540
|
+
Effect15.fail(
|
|
2541
|
+
new RelayError({
|
|
2542
|
+
message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2543
|
+
url,
|
|
2544
|
+
cause: e
|
|
2545
|
+
})
|
|
2546
|
+
)
|
|
2547
|
+
);
|
|
2548
|
+
const handler = (msg) => {
|
|
2549
|
+
if (settled || typeof msg.data !== "string") return;
|
|
2550
|
+
const frameOpt = parseNegMessageFrame(msg.data);
|
|
2551
|
+
if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId) return;
|
|
2552
|
+
const frame = frameOpt.value;
|
|
2233
2553
|
cleanup();
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2554
|
+
if (frame[0] === "NEG-MSG") {
|
|
2555
|
+
resume(
|
|
2556
|
+
Effect15.succeed({
|
|
2557
|
+
msgHex: frame[2],
|
|
2558
|
+
haveIds: [],
|
|
2559
|
+
needIds: []
|
|
2560
|
+
})
|
|
2561
|
+
);
|
|
2239
2562
|
return;
|
|
2563
|
+
}
|
|
2564
|
+
fail(new Error(`NEG-ERR: ${frame[2]}`));
|
|
2565
|
+
};
|
|
2566
|
+
try {
|
|
2567
|
+
sub = relay.subscribe([filter], {
|
|
2568
|
+
onevent() {
|
|
2569
|
+
},
|
|
2570
|
+
oneose() {
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
ws = relay._ws || relay.ws;
|
|
2574
|
+
if (!ws) {
|
|
2575
|
+
cleanup();
|
|
2576
|
+
fail(new Error("Cannot access relay WebSocket"));
|
|
2577
|
+
return Effect15.succeed(void 0);
|
|
2578
|
+
}
|
|
2579
|
+
timer = setTimeout(() => {
|
|
2580
|
+
if (settled) return;
|
|
2581
|
+
cleanup();
|
|
2582
|
+
fail(new Error("NIP-77 negotiation timeout"));
|
|
2583
|
+
}, 3e4);
|
|
2584
|
+
ws.addEventListener("message", handler);
|
|
2585
|
+
ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
|
|
2586
|
+
} catch (e) {
|
|
2240
2587
|
cleanup();
|
|
2241
|
-
fail(
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
cleanup();
|
|
2247
|
-
fail(e);
|
|
2248
|
-
}
|
|
2249
|
-
return Effect15.sync(cleanup);
|
|
2250
|
-
})),
|
|
2588
|
+
fail(e);
|
|
2589
|
+
}
|
|
2590
|
+
return Effect15.sync(cleanup);
|
|
2591
|
+
})
|
|
2592
|
+
),
|
|
2251
2593
|
closeAll: () => ScopedCache.invalidateAll(connections),
|
|
2252
2594
|
getStatus: () => ({ connectedUrls: [...connectedUrls] }),
|
|
2253
2595
|
subscribeStatus: (callback) => {
|
|
@@ -2259,10 +2601,10 @@ function createRelayHandle() {
|
|
|
2259
2601
|
}
|
|
2260
2602
|
|
|
2261
2603
|
// src/layers/RelayLive.ts
|
|
2262
|
-
var RelayLive =
|
|
2604
|
+
var RelayLive = Layer5.effect(Relay, createRelayHandle());
|
|
2263
2605
|
|
|
2264
2606
|
// src/layers/GiftWrapLive.ts
|
|
2265
|
-
import { Effect as Effect17, Layer as
|
|
2607
|
+
import { Effect as Effect17, Layer as Layer6 } from "effect";
|
|
2266
2608
|
|
|
2267
2609
|
// src/sync/gift-wrap.ts
|
|
2268
2610
|
import { Effect as Effect16 } from "effect";
|
|
@@ -2299,14 +2641,17 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
|
2299
2641
|
}
|
|
2300
2642
|
|
|
2301
2643
|
// src/layers/GiftWrapLive.ts
|
|
2302
|
-
var GiftWrapLive =
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2644
|
+
var GiftWrapLive = Layer6.effect(
|
|
2645
|
+
GiftWrap3,
|
|
2646
|
+
Effect17.gen(function* () {
|
|
2647
|
+
const identity = yield* Identity;
|
|
2648
|
+
const epochStore = yield* EpochStore;
|
|
2649
|
+
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2650
|
+
})
|
|
2651
|
+
);
|
|
2307
2652
|
|
|
2308
2653
|
// src/layers/PublishQueueLive.ts
|
|
2309
|
-
import { Effect as Effect19, Layer as
|
|
2654
|
+
import { Effect as Effect19, Layer as Layer7 } from "effect";
|
|
2310
2655
|
|
|
2311
2656
|
// src/sync/publish-queue.ts
|
|
2312
2657
|
import { Effect as Effect18, Ref as Ref4 } from "effect";
|
|
@@ -2317,12 +2662,11 @@ function persist(storage, pending) {
|
|
|
2317
2662
|
function createPublishQueue(storage, relay) {
|
|
2318
2663
|
return Effect18.gen(function* () {
|
|
2319
2664
|
const stored = yield* storage.getMeta(META_KEY);
|
|
2320
|
-
const initial = Array.isArray(stored) ? new Set(stored) : new Set;
|
|
2665
|
+
const initial = Array.isArray(stored) ? new Set(stored) : /* @__PURE__ */ new Set();
|
|
2321
2666
|
const pendingRef = yield* Ref4.make(initial);
|
|
2322
|
-
const listeners = new Set;
|
|
2667
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2323
2668
|
const notify = (pending) => {
|
|
2324
|
-
for (const listener of listeners)
|
|
2325
|
-
listener(pending.size);
|
|
2669
|
+
for (const listener of listeners) listener(pending.size);
|
|
2326
2670
|
};
|
|
2327
2671
|
return {
|
|
2328
2672
|
enqueue: (eventId) => Effect18.gen(function* () {
|
|
@@ -2336,13 +2680,11 @@ function createPublishQueue(storage, relay) {
|
|
|
2336
2680
|
}),
|
|
2337
2681
|
flush: (relayUrls) => Effect18.gen(function* () {
|
|
2338
2682
|
const pending = yield* Ref4.get(pendingRef);
|
|
2339
|
-
if (pending.size === 0)
|
|
2340
|
-
|
|
2341
|
-
const succeeded = new Set;
|
|
2683
|
+
if (pending.size === 0) return;
|
|
2684
|
+
const succeeded = /* @__PURE__ */ new Set();
|
|
2342
2685
|
let consecutiveFailures = 0;
|
|
2343
2686
|
for (const eventId of pending) {
|
|
2344
|
-
if (consecutiveFailures >= 3)
|
|
2345
|
-
break;
|
|
2687
|
+
if (consecutiveFailures >= 3) break;
|
|
2346
2688
|
const gw = yield* storage.getGiftWrap(eventId);
|
|
2347
2689
|
if (!gw || !gw.event) {
|
|
2348
2690
|
succeeded.add(eventId);
|
|
@@ -2352,7 +2694,6 @@ function createPublishQueue(storage, relay) {
|
|
|
2352
2694
|
const result = yield* Effect18.result(relay.publish(gw.event, relayUrls));
|
|
2353
2695
|
if (result._tag === "Success") {
|
|
2354
2696
|
succeeded.add(eventId);
|
|
2355
|
-
yield* storage.stripGiftWrapBlob(eventId);
|
|
2356
2697
|
consecutiveFailures = 0;
|
|
2357
2698
|
} else {
|
|
2358
2699
|
consecutiveFailures++;
|
|
@@ -2380,27 +2721,29 @@ function createPublishQueue(storage, relay) {
|
|
|
2380
2721
|
}
|
|
2381
2722
|
|
|
2382
2723
|
// src/layers/PublishQueueLive.ts
|
|
2383
|
-
var PublishQueueLive =
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2724
|
+
var PublishQueueLive = Layer7.effect(
|
|
2725
|
+
PublishQueue,
|
|
2726
|
+
Effect19.gen(function* () {
|
|
2727
|
+
const storage = yield* Storage;
|
|
2728
|
+
const relay = yield* Relay;
|
|
2729
|
+
return yield* createPublishQueue(storage, relay);
|
|
2730
|
+
})
|
|
2731
|
+
);
|
|
2388
2732
|
|
|
2389
2733
|
// src/layers/SyncStatusLive.ts
|
|
2390
|
-
import { Layer as
|
|
2734
|
+
import { Layer as Layer8 } from "effect";
|
|
2391
2735
|
|
|
2392
2736
|
// src/sync/sync-status.ts
|
|
2393
2737
|
import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
2394
2738
|
function createSyncStatusHandle() {
|
|
2395
2739
|
return Effect20.gen(function* () {
|
|
2396
2740
|
const ref = yield* SubscriptionRef.make("idle");
|
|
2397
|
-
const listeners = new Set;
|
|
2741
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2398
2742
|
return {
|
|
2399
2743
|
get: () => SubscriptionRef.get(ref),
|
|
2400
2744
|
set: (status) => Effect20.gen(function* () {
|
|
2401
2745
|
yield* SubscriptionRef.set(ref, status);
|
|
2402
|
-
for (const listener of listeners)
|
|
2403
|
-
listener(status);
|
|
2746
|
+
for (const listener of listeners) listener(status);
|
|
2404
2747
|
}),
|
|
2405
2748
|
subscribe: (callback) => {
|
|
2406
2749
|
listeners.add(callback);
|
|
@@ -2411,12 +2754,11 @@ function createSyncStatusHandle() {
|
|
|
2411
2754
|
}
|
|
2412
2755
|
|
|
2413
2756
|
// src/layers/SyncStatusLive.ts
|
|
2414
|
-
var SyncStatusLive =
|
|
2757
|
+
var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
|
|
2415
2758
|
|
|
2416
2759
|
// src/layers/TablinumLive.ts
|
|
2417
2760
|
function reportSyncError(onSyncError, error) {
|
|
2418
|
-
if (!onSyncError)
|
|
2419
|
-
return;
|
|
2761
|
+
if (!onSyncError) return;
|
|
2420
2762
|
onSyncError(error instanceof Error ? error : new Error(String(error)));
|
|
2421
2763
|
}
|
|
2422
2764
|
function mapMemberRecord(record) {
|
|
@@ -2424,239 +2766,365 @@ function mapMemberRecord(record) {
|
|
|
2424
2766
|
id: record.id,
|
|
2425
2767
|
addedAt: record.addedAt,
|
|
2426
2768
|
addedInEpoch: record.addedInEpoch,
|
|
2427
|
-
...record.name !==
|
|
2428
|
-
...record.picture !==
|
|
2429
|
-
...record.about !==
|
|
2430
|
-
...record.nip05 !==
|
|
2431
|
-
...record.removedAt !==
|
|
2432
|
-
...record.removedInEpoch !==
|
|
2769
|
+
...record.name !== void 0 ? { name: record.name } : {},
|
|
2770
|
+
...record.picture !== void 0 ? { picture: record.picture } : {},
|
|
2771
|
+
...record.about !== void 0 ? { about: record.about } : {},
|
|
2772
|
+
...record.nip05 !== void 0 ? { nip05: record.nip05 } : {},
|
|
2773
|
+
...record.removedAt !== void 0 ? { removedAt: record.removedAt } : {},
|
|
2774
|
+
...record.removedInEpoch !== void 0 ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2433
2775
|
};
|
|
2434
2776
|
}
|
|
2435
|
-
var IdentityWithDeps = IdentityLive.pipe(
|
|
2436
|
-
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
var
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
const
|
|
2461
|
-
const
|
|
2462
|
-
const
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
const
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2777
|
+
var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
|
|
2778
|
+
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2779
|
+
Layer9.provide(IdentityWithDeps),
|
|
2780
|
+
Layer9.provide(StorageLive)
|
|
2781
|
+
);
|
|
2782
|
+
var GiftWrapWithDeps = GiftWrapLive.pipe(
|
|
2783
|
+
Layer9.provide(IdentityWithDeps),
|
|
2784
|
+
Layer9.provide(EpochStoreWithDeps)
|
|
2785
|
+
);
|
|
2786
|
+
var PublishQueueWithDeps = PublishQueueLive.pipe(
|
|
2787
|
+
Layer9.provide(StorageLive),
|
|
2788
|
+
Layer9.provide(RelayLive)
|
|
2789
|
+
);
|
|
2790
|
+
var AllServicesLive = Layer9.mergeAll(
|
|
2791
|
+
IdentityWithDeps,
|
|
2792
|
+
EpochStoreWithDeps,
|
|
2793
|
+
StorageLive,
|
|
2794
|
+
RelayLive,
|
|
2795
|
+
GiftWrapWithDeps,
|
|
2796
|
+
PublishQueueWithDeps,
|
|
2797
|
+
SyncStatusLive
|
|
2798
|
+
);
|
|
2799
|
+
var TablinumLive = Layer9.effect(
|
|
2800
|
+
Tablinum,
|
|
2801
|
+
Effect21.gen(function* () {
|
|
2802
|
+
const config = yield* Config;
|
|
2803
|
+
const identity = yield* Identity;
|
|
2804
|
+
const epochStore = yield* EpochStore;
|
|
2805
|
+
const storage = yield* Storage;
|
|
2806
|
+
const relay = yield* Relay;
|
|
2807
|
+
const giftWrap = yield* GiftWrap3;
|
|
2808
|
+
const publishQueue = yield* PublishQueue;
|
|
2809
|
+
const syncStatus = yield* SyncStatus;
|
|
2810
|
+
const scope = yield* Effect21.scope;
|
|
2811
|
+
const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
|
|
2812
|
+
const pubsub = yield* PubSub2.unbounded();
|
|
2813
|
+
const replayingRef = yield* Ref5.make(false);
|
|
2814
|
+
const closedRef = yield* Ref5.make(false);
|
|
2815
|
+
const watchCtx = { pubsub, replayingRef };
|
|
2816
|
+
const schemaEntries = Object.entries(config.schema);
|
|
2817
|
+
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2818
|
+
const knownCollections = new Map(
|
|
2819
|
+
allSchemaEntries.map(([, def]) => [def.name, def.eventRetention])
|
|
2820
|
+
);
|
|
2821
|
+
let notifyAuthor;
|
|
2822
|
+
const syncHandle = createSyncHandle(
|
|
2823
|
+
storage,
|
|
2824
|
+
giftWrap,
|
|
2825
|
+
relay,
|
|
2826
|
+
publishQueue,
|
|
2827
|
+
syncStatus,
|
|
2828
|
+
watchCtx,
|
|
2829
|
+
config.relays,
|
|
2830
|
+
knownCollections,
|
|
2831
|
+
epochStore,
|
|
2832
|
+
identity.privateKey,
|
|
2833
|
+
identity.publicKey,
|
|
2834
|
+
scope,
|
|
2835
|
+
config.logLevel,
|
|
2836
|
+
config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : void 0,
|
|
2837
|
+
(pubkey) => notifyAuthor?.(pubkey),
|
|
2838
|
+
config.onRemoved,
|
|
2839
|
+
config.onMembersChanged
|
|
2840
|
+
);
|
|
2841
|
+
const onWrite = (event) => Effect21.gen(function* () {
|
|
2842
|
+
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2843
|
+
const dTag = `${event.collection}:${event.recordId}`;
|
|
2844
|
+
const wrapResult = yield* Effect21.result(
|
|
2845
|
+
giftWrap.wrap({
|
|
2846
|
+
kind: 1,
|
|
2847
|
+
content,
|
|
2848
|
+
tags: [["d", dTag]],
|
|
2849
|
+
created_at: Math.floor(event.createdAt / 1e3)
|
|
2850
|
+
})
|
|
2851
|
+
);
|
|
2852
|
+
if (wrapResult._tag === "Failure") {
|
|
2853
|
+
reportSyncError(config.onSyncError, wrapResult.failure);
|
|
2854
|
+
return;
|
|
2482
2855
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
yield* notifyChange(watchCtx, {
|
|
2501
|
-
collection: "_members",
|
|
2502
|
-
recordId: record.id,
|
|
2503
|
-
kind: existing ? "update" : "create"
|
|
2856
|
+
const gw = wrapResult.success;
|
|
2857
|
+
yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
2858
|
+
yield* Effect21.forkIn(
|
|
2859
|
+
Effect21.gen(function* () {
|
|
2860
|
+
const publishResult = yield* Effect21.result(
|
|
2861
|
+
syncHandle.publishLocal({
|
|
2862
|
+
id: gw.id,
|
|
2863
|
+
event: gw,
|
|
2864
|
+
createdAt: gw.created_at
|
|
2865
|
+
})
|
|
2866
|
+
);
|
|
2867
|
+
if (publishResult._tag === "Failure") {
|
|
2868
|
+
reportSyncError(config.onSyncError, publishResult.failure);
|
|
2869
|
+
}
|
|
2870
|
+
}),
|
|
2871
|
+
scope
|
|
2872
|
+
);
|
|
2504
2873
|
});
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
...profileOpt.value
|
|
2527
|
-
});
|
|
2528
|
-
yield* notifyChange(watchCtx, {
|
|
2529
|
-
collection: "_members",
|
|
2530
|
-
recordId: pubkey,
|
|
2531
|
-
kind: "update"
|
|
2532
|
-
});
|
|
2533
|
-
config.onMembersChanged?.();
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2536
|
-
}).pipe(Effect21.ignore, Effect21.forkIn(scope)));
|
|
2537
|
-
};
|
|
2538
|
-
const handles = new Map;
|
|
2539
|
-
for (const [, def] of allSchemaEntries) {
|
|
2540
|
-
const validator = buildValidator(def.name, def);
|
|
2541
|
-
const partialValidator = buildPartialValidator(def.name, def);
|
|
2542
|
-
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite);
|
|
2543
|
-
handles.set(def.name, handle);
|
|
2544
|
-
}
|
|
2545
|
-
yield* syncHandle.startSubscription();
|
|
2546
|
-
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2547
|
-
if (!selfMember) {
|
|
2548
|
-
yield* putMemberRecord({
|
|
2549
|
-
id: identity.publicKey,
|
|
2550
|
-
addedAt: Date.now(),
|
|
2551
|
-
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2874
|
+
const knownAuthors = /* @__PURE__ */ new Set();
|
|
2875
|
+
const putMemberRecord = (record) => Effect21.gen(function* () {
|
|
2876
|
+
const existing = yield* storage.getRecord("_members", record.id);
|
|
2877
|
+
const event = {
|
|
2878
|
+
id: uuidv7(),
|
|
2879
|
+
collection: "_members",
|
|
2880
|
+
recordId: record.id,
|
|
2881
|
+
kind: existing ? "u" : "c",
|
|
2882
|
+
data: record,
|
|
2883
|
+
createdAt: Date.now(),
|
|
2884
|
+
author: identity.publicKey
|
|
2885
|
+
};
|
|
2886
|
+
yield* storage.putEvent(event);
|
|
2887
|
+
yield* applyEvent(storage, event);
|
|
2888
|
+
yield* onWrite(event);
|
|
2889
|
+
yield* notifyChange(watchCtx, {
|
|
2890
|
+
collection: "_members",
|
|
2891
|
+
recordId: record.id,
|
|
2892
|
+
kind: existing ? "update" : "create"
|
|
2893
|
+
});
|
|
2894
|
+
config.onMembersChanged?.();
|
|
2552
2895
|
});
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2896
|
+
notifyAuthor = (pubkey) => {
|
|
2897
|
+
if (knownAuthors.has(pubkey)) return;
|
|
2898
|
+
knownAuthors.add(pubkey);
|
|
2899
|
+
Effect21.runFork(
|
|
2900
|
+
Effect21.gen(function* () {
|
|
2901
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
2902
|
+
if (!existing) {
|
|
2903
|
+
yield* putMemberRecord({
|
|
2904
|
+
id: pubkey,
|
|
2905
|
+
addedAt: Date.now(),
|
|
2906
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(
|
|
2910
|
+
Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none()))
|
|
2911
|
+
);
|
|
2912
|
+
if (Option9.isSome(profileOpt)) {
|
|
2913
|
+
const current = yield* storage.getRecord("_members", pubkey);
|
|
2914
|
+
if (current) {
|
|
2915
|
+
yield* storage.putRecord("_members", {
|
|
2916
|
+
...current,
|
|
2917
|
+
...profileOpt.value
|
|
2918
|
+
});
|
|
2919
|
+
yield* notifyChange(watchCtx, {
|
|
2920
|
+
collection: "_members",
|
|
2921
|
+
recordId: pubkey,
|
|
2922
|
+
kind: "update"
|
|
2923
|
+
});
|
|
2924
|
+
config.onMembersChanged?.();
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope))
|
|
2928
|
+
);
|
|
2929
|
+
};
|
|
2930
|
+
const handles = /* @__PURE__ */ new Map();
|
|
2931
|
+
for (const [, def] of allSchemaEntries) {
|
|
2932
|
+
const validator = buildValidator(def.name, def);
|
|
2933
|
+
const partialValidator = buildPartialValidator(def.name, def);
|
|
2934
|
+
const handle = createCollectionHandle(
|
|
2935
|
+
def,
|
|
2936
|
+
storage,
|
|
2937
|
+
watchCtx,
|
|
2938
|
+
validator,
|
|
2939
|
+
partialValidator,
|
|
2940
|
+
uuidv7,
|
|
2941
|
+
identity.publicKey,
|
|
2942
|
+
onWrite,
|
|
2943
|
+
config.logLevel
|
|
2944
|
+
);
|
|
2945
|
+
handles.set(def.name, handle);
|
|
2946
|
+
}
|
|
2947
|
+
yield* syncHandle.startSubscription();
|
|
2948
|
+
yield* Effect21.logInfo("Tablinum ready", {
|
|
2949
|
+
dbName: config.dbName,
|
|
2950
|
+
collections: schemaEntries.map(([name]) => name),
|
|
2951
|
+
relays: config.relays
|
|
2952
|
+
});
|
|
2953
|
+
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2954
|
+
if (!selfMember) {
|
|
2599
2955
|
yield* putMemberRecord({
|
|
2600
|
-
id:
|
|
2956
|
+
id: identity.publicKey,
|
|
2601
2957
|
addedAt: Date.now(),
|
|
2602
|
-
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2603
|
-
...existing ? { removedAt: undefined, removedInEpoch: undefined } : {}
|
|
2604
|
-
});
|
|
2605
|
-
})),
|
|
2606
|
-
removeMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
|
|
2607
|
-
const allMembers = yield* storage.getAllRecords("_members");
|
|
2608
|
-
const activeMembers = allMembers.filter((member) => !member.removedAt && member.id !== pubkey);
|
|
2609
|
-
const activePubkeys = activeMembers.map((member) => member.id);
|
|
2610
|
-
const result = createRotation(epochStore, identity.privateKey, identity.publicKey, activePubkeys, [pubkey]);
|
|
2611
|
-
addEpoch(epochStore, result.epoch);
|
|
2612
|
-
epochStore.currentEpochId = result.epoch.id;
|
|
2613
|
-
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
2614
|
-
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
2615
|
-
yield* putMemberRecord({
|
|
2616
|
-
...memberRecord ?? {
|
|
2617
|
-
id: pubkey,
|
|
2618
|
-
addedAt: 0,
|
|
2619
|
-
addedInEpoch: EpochId("epoch-0")
|
|
2620
|
-
},
|
|
2621
|
-
removedAt: Date.now(),
|
|
2622
|
-
removedInEpoch: result.epoch.id
|
|
2958
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2623
2959
|
});
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2960
|
+
}
|
|
2961
|
+
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
2962
|
+
const ensureOpen = (effect) => withLog(
|
|
2963
|
+
Effect21.gen(function* () {
|
|
2964
|
+
if (yield* Ref5.get(closedRef)) {
|
|
2965
|
+
return yield* new StorageError({ message: "Database is closed" });
|
|
2966
|
+
}
|
|
2967
|
+
return yield* effect;
|
|
2968
|
+
})
|
|
2969
|
+
);
|
|
2970
|
+
const ensureSyncOpen = (effect) => withLog(
|
|
2971
|
+
Effect21.gen(function* () {
|
|
2972
|
+
if (yield* Ref5.get(closedRef)) {
|
|
2973
|
+
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
2974
|
+
}
|
|
2975
|
+
return yield* effect;
|
|
2976
|
+
})
|
|
2977
|
+
);
|
|
2978
|
+
const dbHandle = {
|
|
2979
|
+
collection: (name) => {
|
|
2980
|
+
const handle = handles.get(name);
|
|
2981
|
+
if (!handle) throw new Error(`Collection "${name}" not found in schema`);
|
|
2982
|
+
return handle;
|
|
2983
|
+
},
|
|
2984
|
+
publicKey: identity.publicKey,
|
|
2985
|
+
members: handles.get("_members"),
|
|
2986
|
+
exportKey: () => identity.exportKey(),
|
|
2987
|
+
exportInvite: () => ({
|
|
2988
|
+
epochKeys: [...exportEpochKeys(epochStore)],
|
|
2989
|
+
relays: [...config.relays],
|
|
2990
|
+
dbName: config.dbName
|
|
2991
|
+
}),
|
|
2992
|
+
close: () => withLog(
|
|
2993
|
+
Effect21.gen(function* () {
|
|
2994
|
+
if (yield* Ref5.get(closedRef)) return;
|
|
2995
|
+
yield* Ref5.set(closedRef, true);
|
|
2996
|
+
syncHandle.stopHealing();
|
|
2997
|
+
yield* Scope4.close(scope, Exit.void);
|
|
2998
|
+
})
|
|
2999
|
+
),
|
|
3000
|
+
rebuild: () => ensureOpen(
|
|
3001
|
+
rebuild(
|
|
3002
|
+
storage,
|
|
3003
|
+
allSchemaEntries.map(([, def]) => def.name)
|
|
3004
|
+
)
|
|
3005
|
+
),
|
|
3006
|
+
sync: () => ensureSyncOpen(
|
|
3007
|
+
syncHandle.sync().pipe(
|
|
3008
|
+
Effect21.tap(
|
|
3009
|
+
() => Effect21.sync(() => {
|
|
3010
|
+
syncHandle.startHealing();
|
|
3011
|
+
})
|
|
3012
|
+
)
|
|
3013
|
+
)
|
|
3014
|
+
),
|
|
3015
|
+
getSyncStatus: () => syncStatus.get(),
|
|
3016
|
+
subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
|
|
3017
|
+
pendingCount: () => publishQueue.size(),
|
|
3018
|
+
subscribePendingCount: (callback) => publishQueue.subscribe(callback),
|
|
3019
|
+
getRelayStatus: () => relay.getStatus(),
|
|
3020
|
+
subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
|
|
3021
|
+
addMember: (pubkey) => ensureOpen(
|
|
3022
|
+
Effect21.gen(function* () {
|
|
3023
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
3024
|
+
if (existing && !existing.removedAt) return;
|
|
3025
|
+
yield* putMemberRecord({
|
|
3026
|
+
id: pubkey,
|
|
3027
|
+
addedAt: Date.now(),
|
|
3028
|
+
addedInEpoch: getCurrentEpoch(epochStore).id,
|
|
3029
|
+
...existing ? { removedAt: void 0, removedInEpoch: void 0 } : {}
|
|
3030
|
+
});
|
|
3031
|
+
})
|
|
3032
|
+
),
|
|
3033
|
+
removeMember: (pubkey) => ensureOpen(
|
|
3034
|
+
Effect21.gen(function* () {
|
|
3035
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
3036
|
+
const activeMembers = allMembers.filter(
|
|
3037
|
+
(member) => !member.removedAt && member.id !== pubkey
|
|
3038
|
+
);
|
|
3039
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
3040
|
+
const result = createRotation(
|
|
3041
|
+
epochStore,
|
|
3042
|
+
identity.privateKey,
|
|
3043
|
+
identity.publicKey,
|
|
3044
|
+
activePubkeys,
|
|
3045
|
+
[pubkey]
|
|
3046
|
+
);
|
|
3047
|
+
addEpoch(epochStore, result.epoch);
|
|
3048
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
3049
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
3050
|
+
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
3051
|
+
yield* putMemberRecord({
|
|
3052
|
+
...memberRecord ?? {
|
|
3053
|
+
id: pubkey,
|
|
3054
|
+
addedAt: 0,
|
|
3055
|
+
addedInEpoch: EpochId("epoch-0")
|
|
3056
|
+
},
|
|
3057
|
+
removedAt: Date.now(),
|
|
3058
|
+
removedInEpoch: result.epoch.id
|
|
3059
|
+
});
|
|
3060
|
+
yield* Effect21.forEach(
|
|
3061
|
+
result.wrappedEvents,
|
|
3062
|
+
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
3063
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3064
|
+
Effect21.ignore
|
|
3065
|
+
),
|
|
3066
|
+
{ discard: true }
|
|
3067
|
+
);
|
|
3068
|
+
yield* Effect21.forEach(
|
|
3069
|
+
result.removalNotices,
|
|
3070
|
+
(notice) => relay.publish(notice, [...config.relays]).pipe(
|
|
3071
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3072
|
+
Effect21.ignore
|
|
3073
|
+
),
|
|
3074
|
+
{ discard: true }
|
|
3075
|
+
);
|
|
3076
|
+
yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
|
|
3077
|
+
})
|
|
3078
|
+
),
|
|
3079
|
+
getMembers: () => ensureOpen(
|
|
3080
|
+
Effect21.gen(function* () {
|
|
3081
|
+
const allRecords = yield* storage.getAllRecords("_members");
|
|
3082
|
+
return allRecords.filter((record) => !record._d).map(mapMemberRecord);
|
|
3083
|
+
})
|
|
3084
|
+
),
|
|
3085
|
+
getProfile: () => ensureOpen(
|
|
3086
|
+
Effect21.gen(function* () {
|
|
3087
|
+
const record = yield* storage.getRecord("_members", identity.publicKey);
|
|
3088
|
+
if (!record) return {};
|
|
3089
|
+
const profile = {};
|
|
3090
|
+
if (record.name !== void 0) profile.name = record.name;
|
|
3091
|
+
if (record.picture !== void 0) profile.picture = record.picture;
|
|
3092
|
+
if (record.about !== void 0) profile.about = record.about;
|
|
3093
|
+
if (record.nip05 !== void 0) profile.nip05 = record.nip05;
|
|
3094
|
+
return profile;
|
|
3095
|
+
})
|
|
3096
|
+
),
|
|
3097
|
+
setProfile: (profile) => ensureOpen(
|
|
3098
|
+
Effect21.gen(function* () {
|
|
3099
|
+
const existing = yield* storage.getRecord("_members", identity.publicKey);
|
|
3100
|
+
if (!existing) {
|
|
3101
|
+
return yield* new ValidationError({ message: "Current user is not a member" });
|
|
3102
|
+
}
|
|
3103
|
+
const { _d, _u, _a, _e, ...memberFields } = existing;
|
|
3104
|
+
yield* putMemberRecord({ ...memberFields, ...profile });
|
|
3105
|
+
})
|
|
3106
|
+
)
|
|
3107
|
+
};
|
|
3108
|
+
return dbHandle;
|
|
3109
|
+
}).pipe(Effect21.withLogSpan("tablinum.init"))
|
|
3110
|
+
).pipe(Layer9.provide(AllServicesLive));
|
|
2658
3111
|
|
|
2659
3112
|
// src/db/create-tablinum.ts
|
|
3113
|
+
function resolveLogLevel(input) {
|
|
3114
|
+
if (input === void 0 || input === "none") return "None";
|
|
3115
|
+
switch (input) {
|
|
3116
|
+
case "debug":
|
|
3117
|
+
return "Debug";
|
|
3118
|
+
case "info":
|
|
3119
|
+
return "Info";
|
|
3120
|
+
case "warning":
|
|
3121
|
+
return "Warn";
|
|
3122
|
+
case "error":
|
|
3123
|
+
return "Error";
|
|
3124
|
+
default:
|
|
3125
|
+
return input;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
2660
3128
|
function validateConfig(config) {
|
|
2661
3129
|
return Effect22.gen(function* () {
|
|
2662
3130
|
if (Object.keys(config.schema).length === 0) {
|
|
@@ -2670,19 +3138,26 @@ function createTablinum(config) {
|
|
|
2670
3138
|
return Effect22.gen(function* () {
|
|
2671
3139
|
yield* validateConfig(config);
|
|
2672
3140
|
const runtimeConfig = yield* resolveRuntimeConfig(config);
|
|
3141
|
+
const logLevel = resolveLogLevel(config.logLevel);
|
|
2673
3142
|
const configValue = {
|
|
2674
3143
|
...runtimeConfig,
|
|
2675
3144
|
schema: config.schema,
|
|
3145
|
+
logLevel,
|
|
2676
3146
|
onSyncError: config.onSyncError,
|
|
2677
3147
|
onRemoved: config.onRemoved,
|
|
2678
3148
|
onMembersChanged: config.onMembersChanged
|
|
2679
3149
|
};
|
|
2680
|
-
const configLayer =
|
|
2681
|
-
const
|
|
2682
|
-
const
|
|
3150
|
+
const configLayer = Layer10.succeed(Config, configValue);
|
|
3151
|
+
const logLayer = Layer10.succeed(References4.MinimumLogLevel, logLevel);
|
|
3152
|
+
const fullLayer = TablinumLive.pipe(Layer10.provide(configLayer), Layer10.provide(logLayer));
|
|
3153
|
+
const ctx = yield* Layer10.build(fullLayer);
|
|
2683
3154
|
return ServiceMap10.get(ctx, Tablinum);
|
|
2684
3155
|
});
|
|
2685
3156
|
}
|
|
3157
|
+
|
|
3158
|
+
// src/index.ts
|
|
3159
|
+
import { LogLevel } from "effect";
|
|
3160
|
+
|
|
2686
3161
|
// src/db/invite.ts
|
|
2687
3162
|
import { Schema as Schema7 } from "effect";
|
|
2688
3163
|
var InviteSchema = Schema7.Struct({
|
|
@@ -2717,19 +3192,20 @@ function decodeInvite(encoded) {
|
|
|
2717
3192
|
}
|
|
2718
3193
|
}
|
|
2719
3194
|
export {
|
|
2720
|
-
|
|
2721
|
-
encodeInvite,
|
|
2722
|
-
decodeInvite,
|
|
2723
|
-
createTablinum,
|
|
2724
|
-
collection,
|
|
2725
|
-
ValidationError,
|
|
2726
|
-
TablinumLive,
|
|
2727
|
-
SyncError,
|
|
2728
|
-
StorageError,
|
|
2729
|
-
RelayError,
|
|
2730
|
-
NotFoundError,
|
|
2731
|
-
EpochId,
|
|
2732
|
-
DatabaseName,
|
|
3195
|
+
ClosedError,
|
|
2733
3196
|
CryptoError,
|
|
2734
|
-
|
|
3197
|
+
DatabaseName,
|
|
3198
|
+
EpochId,
|
|
3199
|
+
LogLevel,
|
|
3200
|
+
NotFoundError,
|
|
3201
|
+
RelayError,
|
|
3202
|
+
StorageError,
|
|
3203
|
+
SyncError,
|
|
3204
|
+
TablinumLive,
|
|
3205
|
+
ValidationError,
|
|
3206
|
+
collection,
|
|
3207
|
+
createTablinum,
|
|
3208
|
+
decodeInvite,
|
|
3209
|
+
encodeInvite,
|
|
3210
|
+
field
|
|
2735
3211
|
};
|