tablinum 0.1.3 → 0.4.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/README.md +134 -85
- package/dist/brands.d.ts +5 -0
- package/dist/crud/collection-handle.d.ts +8 -6
- package/dist/crud/query-builder.d.ts +4 -13
- package/dist/crud/watch.d.ts +0 -10
- package/dist/db/create-tablinum.d.ts +7 -0
- package/dist/db/database-handle.d.ts +23 -1
- package/dist/db/epoch.d.ts +48 -0
- package/dist/db/invite.d.ts +8 -0
- package/dist/db/key-rotation.d.ts +24 -0
- package/dist/db/members.d.ts +24 -0
- package/dist/db/runtime-config.d.ts +16 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1744 -842
- package/dist/layers/EpochStoreLive.d.ts +6 -0
- package/dist/layers/GiftWrapLive.d.ts +5 -0
- package/dist/layers/IdentityLive.d.ts +5 -0
- package/dist/layers/PublishQueueLive.d.ts +5 -0
- package/dist/layers/RelayLive.d.ts +3 -0
- package/dist/layers/StorageLive.d.ts +4 -0
- package/dist/layers/SyncStatusLive.d.ts +3 -0
- package/dist/layers/TablinumLive.d.ts +5 -0
- package/dist/layers/index.d.ts +8 -0
- package/dist/schema/collection.d.ts +2 -0
- package/dist/schema/field.d.ts +7 -2
- package/dist/schema/types.d.ts +0 -4
- package/dist/services/Config.d.ts +16 -0
- package/dist/services/EpochStore.d.ts +6 -0
- package/dist/services/GiftWrap.d.ts +6 -0
- package/dist/services/Identity.d.ts +6 -0
- package/dist/services/PublishQueue.d.ts +6 -0
- package/dist/services/Relay.d.ts +6 -0
- package/dist/services/Storage.d.ts +6 -0
- package/dist/services/Sync.d.ts +6 -0
- package/dist/services/SyncStatus.d.ts +6 -0
- package/dist/services/Tablinum.d.ts +7 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/storage/idb.d.ts +13 -3
- package/dist/storage/lww.d.ts +0 -5
- package/dist/storage/records-store.d.ts +1 -7
- package/dist/svelte/collection.svelte.d.ts +10 -6
- package/dist/svelte/deferred.d.ts +7 -0
- package/dist/svelte/index.svelte.d.ts +6 -6
- package/dist/svelte/index.svelte.js +2116 -1084
- package/dist/svelte/query.svelte.d.ts +6 -6
- package/dist/svelte/tablinum.svelte.d.ts +36 -0
- package/dist/sync/gift-wrap.d.ts +6 -2
- package/dist/sync/negentropy.d.ts +1 -1
- package/dist/sync/publish-queue.d.ts +3 -2
- package/dist/sync/relay.d.ts +9 -2
- package/dist/sync/sync-service.d.ts +5 -2
- package/dist/sync/sync-status.d.ts +1 -0
- package/dist/utils/diff.d.ts +2 -0
- package/dist/utils/uuid.d.ts +0 -1
- package/package.json +10 -7
- package/dist/main.d.ts +0 -1
- package/dist/storage/events-store.d.ts +0 -6
- package/dist/storage/giftwraps-store.d.ts +0 -6
- package/dist/svelte/database.svelte.d.ts +0 -15
- package/dist/svelte/live-query.svelte.d.ts +0 -8
|
@@ -6,15 +6,193 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
// src/
|
|
10
|
-
|
|
9
|
+
// src/schema/field.ts
|
|
10
|
+
function make(kind, isOptional, isArray, fields) {
|
|
11
|
+
return { _tag: "FieldDef", kind, isOptional, isArray, ...fields !== undefined && { fields } };
|
|
12
|
+
}
|
|
13
|
+
var field = {
|
|
14
|
+
string: () => make("string", false, false),
|
|
15
|
+
number: () => make("number", false, false),
|
|
16
|
+
boolean: () => make("boolean", false, false),
|
|
17
|
+
json: () => make("json", false, false),
|
|
18
|
+
object: (fields) => make("object", false, false, fields),
|
|
19
|
+
optional: (inner) => make(inner.kind, true, inner.isArray, inner.fields),
|
|
20
|
+
array: (inner) => make(inner.kind, inner.isOptional, true, inner.fields)
|
|
21
|
+
};
|
|
22
|
+
// src/schema/collection.ts
|
|
23
|
+
var RESERVED_NAMES = new Set(["id", "_d", "_u", "_e", "_a"]);
|
|
24
|
+
function collection(name, fields, options) {
|
|
25
|
+
if (!name || name.trim().length === 0) {
|
|
26
|
+
throw new Error("Collection name must not be empty");
|
|
27
|
+
}
|
|
28
|
+
const fieldNames = Object.keys(fields);
|
|
29
|
+
if (fieldNames.length === 0) {
|
|
30
|
+
throw new Error(`Collection "${name}" must have at least one field`);
|
|
31
|
+
}
|
|
32
|
+
for (const fieldName of fieldNames) {
|
|
33
|
+
if (RESERVED_NAMES.has(fieldName)) {
|
|
34
|
+
throw new Error(`Field name "${fieldName}" is reserved`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const indices = [];
|
|
38
|
+
if (options?.indices) {
|
|
39
|
+
for (const idx of options.indices) {
|
|
40
|
+
const fieldDef = fields[idx];
|
|
41
|
+
if (!fieldDef) {
|
|
42
|
+
throw new Error(`Index field "${idx}" does not exist in collection "${name}"`);
|
|
43
|
+
}
|
|
44
|
+
if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
|
|
45
|
+
throw new Error(`Field "${idx}" cannot be indexed (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`);
|
|
46
|
+
}
|
|
47
|
+
indices.push(idx);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const eventRetention = options?.eventRetention ?? 1;
|
|
51
|
+
if (eventRetention < 0 || !Number.isInteger(eventRetention)) {
|
|
52
|
+
throw new Error(`eventRetention must be a non-negative integer, got ${eventRetention}`);
|
|
53
|
+
}
|
|
54
|
+
return { _tag: "CollectionDef", name, fields, indices, eventRetention };
|
|
55
|
+
}
|
|
56
|
+
// src/db/invite.ts
|
|
57
|
+
import { Schema as Schema2 } from "effect";
|
|
11
58
|
|
|
12
|
-
// src/db/
|
|
13
|
-
import {
|
|
59
|
+
// src/db/epoch.ts
|
|
60
|
+
import { Option, Schema } from "effect";
|
|
61
|
+
import { getPublicKey } from "nostr-tools/pure";
|
|
62
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
14
63
|
|
|
15
|
-
// src/
|
|
16
|
-
import {
|
|
64
|
+
// src/brands.ts
|
|
65
|
+
import { Brand } from "effect";
|
|
66
|
+
var EpochId = Brand.nominal();
|
|
67
|
+
var DatabaseName = Brand.nominal();
|
|
68
|
+
|
|
69
|
+
// src/db/epoch.ts
|
|
70
|
+
var HexKeySchema = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/i));
|
|
71
|
+
var EpochKeyInputSchema = Schema.Struct({
|
|
72
|
+
epochId: Schema.String,
|
|
73
|
+
key: HexKeySchema
|
|
74
|
+
});
|
|
75
|
+
var PersistedEpochSchema = Schema.Struct({
|
|
76
|
+
id: Schema.String,
|
|
77
|
+
privateKey: HexKeySchema,
|
|
78
|
+
createdBy: Schema.String,
|
|
79
|
+
parentEpoch: Schema.optionalKey(Schema.String)
|
|
80
|
+
});
|
|
81
|
+
var PersistedEpochStoreSchema = Schema.Struct({
|
|
82
|
+
epochs: Schema.Array(PersistedEpochSchema),
|
|
83
|
+
currentEpochId: Schema.String
|
|
84
|
+
});
|
|
85
|
+
var decodePersistedEpochStore = Schema.decodeUnknownSync(Schema.fromJsonString(PersistedEpochStoreSchema));
|
|
86
|
+
function createEpochKey(id, privateKeyHex, createdBy, parentEpoch) {
|
|
87
|
+
const publicKey = getPublicKey(hexToBytes(privateKeyHex));
|
|
88
|
+
const base = { id, privateKey: privateKeyHex, publicKey, createdBy };
|
|
89
|
+
return parentEpoch !== undefined ? { ...base, parentEpoch } : base;
|
|
90
|
+
}
|
|
91
|
+
function createEpochStore(initialEpoch) {
|
|
92
|
+
const epochs = new Map;
|
|
93
|
+
const keysByPublicKey = new Map;
|
|
94
|
+
epochs.set(initialEpoch.id, initialEpoch);
|
|
95
|
+
keysByPublicKey.set(initialEpoch.publicKey, hexToBytes(initialEpoch.privateKey));
|
|
96
|
+
return { epochs, keysByPublicKey, currentEpochId: initialEpoch.id };
|
|
97
|
+
}
|
|
98
|
+
function addEpoch(store, epoch) {
|
|
99
|
+
store.epochs.set(epoch.id, epoch);
|
|
100
|
+
store.keysByPublicKey.set(epoch.publicKey, hexToBytes(epoch.privateKey));
|
|
101
|
+
}
|
|
102
|
+
function hydrateEpochStore(snapshot) {
|
|
103
|
+
const [firstEpoch, ...remainingEpochs] = snapshot.epochs.map((epoch) => createEpochKey(EpochId(epoch.id), epoch.privateKey, epoch.createdBy, epoch.parentEpoch !== undefined ? EpochId(epoch.parentEpoch) : undefined));
|
|
104
|
+
if (!firstEpoch) {
|
|
105
|
+
throw new Error("Epoch snapshot must contain at least one epoch");
|
|
106
|
+
}
|
|
107
|
+
const store = createEpochStore(firstEpoch);
|
|
108
|
+
for (const epoch of remainingEpochs) {
|
|
109
|
+
addEpoch(store, epoch);
|
|
110
|
+
}
|
|
111
|
+
store.currentEpochId = EpochId(snapshot.currentEpochId);
|
|
112
|
+
return store;
|
|
113
|
+
}
|
|
114
|
+
function createEpochStoreFromInputs(epochKeys, options = {}) {
|
|
115
|
+
if (epochKeys.length === 0) {
|
|
116
|
+
throw new Error("Epoch input must contain at least one key");
|
|
117
|
+
}
|
|
118
|
+
const createdBy = options.createdBy ?? "";
|
|
119
|
+
const epochs = epochKeys.map((epochKey, index) => createEpochKey(epochKey.epochId, epochKey.key, createdBy, index > 0 ? epochKeys[index - 1].epochId : undefined));
|
|
120
|
+
const store = createEpochStore(epochs[0]);
|
|
121
|
+
for (let i = 1;i < epochs.length; i++) {
|
|
122
|
+
addEpoch(store, epochs[i]);
|
|
123
|
+
}
|
|
124
|
+
store.currentEpochId = epochs[epochs.length - 1].id;
|
|
125
|
+
return store;
|
|
126
|
+
}
|
|
127
|
+
function getCurrentEpoch(store) {
|
|
128
|
+
return store.epochs.get(store.currentEpochId);
|
|
129
|
+
}
|
|
130
|
+
function getCurrentPublicKey(store) {
|
|
131
|
+
return getCurrentEpoch(store).publicKey;
|
|
132
|
+
}
|
|
133
|
+
function getAllPublicKeys(store) {
|
|
134
|
+
return Array.from(store.keysByPublicKey.keys());
|
|
135
|
+
}
|
|
136
|
+
function getDecryptionKey(store, publicKey) {
|
|
137
|
+
return store.keysByPublicKey.get(publicKey);
|
|
138
|
+
}
|
|
139
|
+
function exportEpochKeys(store) {
|
|
140
|
+
return Array.from(store.epochs.values()).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((epoch) => ({ epochId: epoch.id, key: epoch.privateKey }));
|
|
141
|
+
}
|
|
142
|
+
function serializeEpochStore(store) {
|
|
143
|
+
return {
|
|
144
|
+
epochs: Array.from(store.epochs.values()).map((epoch) => ({
|
|
145
|
+
id: epoch.id,
|
|
146
|
+
privateKey: epoch.privateKey,
|
|
147
|
+
createdBy: epoch.createdBy,
|
|
148
|
+
...epoch.parentEpoch !== undefined ? { parentEpoch: epoch.parentEpoch } : {}
|
|
149
|
+
})),
|
|
150
|
+
currentEpochId: store.currentEpochId
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function stringifyEpochStore(store) {
|
|
154
|
+
return JSON.stringify(serializeEpochStore(store));
|
|
155
|
+
}
|
|
156
|
+
function deserializeEpochStore(raw) {
|
|
157
|
+
try {
|
|
158
|
+
return Option.some(hydrateEpochStore(decodePersistedEpochStore(raw)));
|
|
159
|
+
} catch {
|
|
160
|
+
return Option.none();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
17
163
|
|
|
164
|
+
// src/db/invite.ts
|
|
165
|
+
var InviteSchema = Schema2.Struct({
|
|
166
|
+
epochKeys: Schema2.Array(EpochKeyInputSchema),
|
|
167
|
+
relays: Schema2.Array(Schema2.String),
|
|
168
|
+
dbName: Schema2.String
|
|
169
|
+
});
|
|
170
|
+
var decodeInviteJson = Schema2.decodeUnknownSync(Schema2.UnknownFromJsonString);
|
|
171
|
+
var decodeInvitePayload = Schema2.decodeUnknownSync(InviteSchema);
|
|
172
|
+
function encodeInvite(invite) {
|
|
173
|
+
return btoa(JSON.stringify(invite));
|
|
174
|
+
}
|
|
175
|
+
function decodeInvite(encoded) {
|
|
176
|
+
let raw;
|
|
177
|
+
try {
|
|
178
|
+
raw = decodeInviteJson(atob(encoded));
|
|
179
|
+
} catch {
|
|
180
|
+
throw new Error("Invalid invite: failed to decode");
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const invite = decodeInvitePayload(raw);
|
|
184
|
+
return {
|
|
185
|
+
epochKeys: invite.epochKeys.map((epoch) => ({
|
|
186
|
+
epochId: EpochId(epoch.epochId),
|
|
187
|
+
key: epoch.key
|
|
188
|
+
})),
|
|
189
|
+
relays: [...invite.relays],
|
|
190
|
+
dbName: DatabaseName(invite.dbName)
|
|
191
|
+
};
|
|
192
|
+
} catch {
|
|
193
|
+
throw new Error("Invalid invite: unexpected shape");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
18
196
|
// src/errors.ts
|
|
19
197
|
import { Data } from "effect";
|
|
20
198
|
|
|
@@ -38,211 +216,79 @@ class NotFoundError extends Data.TaggedError("NotFoundError") {
|
|
|
38
216
|
|
|
39
217
|
class ClosedError extends Data.TaggedError("ClosedError") {
|
|
40
218
|
}
|
|
219
|
+
// src/svelte/tablinum.svelte.ts
|
|
220
|
+
import { Effect as Effect25, Exit as Exit2, Scope as Scope6 } from "effect";
|
|
41
221
|
|
|
42
|
-
// src/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
base = Schema.UndefinedOr(base);
|
|
64
|
-
}
|
|
65
|
-
return base;
|
|
66
|
-
}
|
|
67
|
-
function buildValidator(collectionName, def) {
|
|
68
|
-
const schemaFields = {
|
|
69
|
-
id: Schema.String
|
|
70
|
-
};
|
|
71
|
-
for (const [name, fieldDef] of Object.entries(def.fields)) {
|
|
72
|
-
schemaFields[name] = fieldDefToSchema(fieldDef);
|
|
73
|
-
}
|
|
74
|
-
const recordSchema = Schema.Struct(schemaFields);
|
|
75
|
-
const decode = Schema.decodeUnknownSync(recordSchema);
|
|
76
|
-
return (input) => Effect.gen(function* () {
|
|
77
|
-
try {
|
|
78
|
-
const result = decode(input);
|
|
79
|
-
return result;
|
|
80
|
-
} catch (e) {
|
|
81
|
-
return yield* new ValidationError({
|
|
82
|
-
message: `Validation failed for collection "${collectionName}": ${e instanceof Error ? e.message : String(e)}`
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
function buildPartialValidator(collectionName, def) {
|
|
88
|
-
return (input) => Effect.gen(function* () {
|
|
89
|
-
if (typeof input !== "object" || input === null) {
|
|
90
|
-
return yield* new ValidationError({
|
|
91
|
-
message: `Validation failed for collection "${collectionName}": expected an object`
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
const record = input;
|
|
95
|
-
for (const [key, value] of Object.entries(record)) {
|
|
96
|
-
const fieldDef = def.fields[key];
|
|
97
|
-
if (!fieldDef) {
|
|
98
|
-
return yield* new ValidationError({
|
|
99
|
-
message: `Unknown field "${key}" in collection "${collectionName}"`,
|
|
100
|
-
field: key
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
if (value === undefined && fieldDef.isOptional) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const fieldSchema = fieldDefToSchema(fieldDef);
|
|
107
|
-
const decode = Schema.decodeUnknownSync(fieldSchema);
|
|
108
|
-
try {
|
|
109
|
-
decode(value);
|
|
110
|
-
} catch (e) {
|
|
111
|
-
return yield* new ValidationError({
|
|
112
|
-
message: `Validation failed for field "${key}" in collection "${collectionName}": ${e instanceof Error ? e.message : String(e)}`,
|
|
113
|
-
field: key
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return record;
|
|
118
|
-
});
|
|
222
|
+
// src/db/create-tablinum.ts
|
|
223
|
+
import { Effect as Effect22, Layer as Layer9, ServiceMap as ServiceMap10 } from "effect";
|
|
224
|
+
|
|
225
|
+
// src/db/runtime-config.ts
|
|
226
|
+
import { Effect, Schema as Schema3 } from "effect";
|
|
227
|
+
var PrivateKeySchema = Schema3.Uint8Array.check(Schema3.isMinLength(32), Schema3.isMaxLength(32));
|
|
228
|
+
var RuntimeConfigSchema = Schema3.Struct({
|
|
229
|
+
relays: Schema3.NonEmptyArray(Schema3.String),
|
|
230
|
+
dbName: Schema3.optional(Schema3.String),
|
|
231
|
+
privateKey: Schema3.optional(PrivateKeySchema),
|
|
232
|
+
epochKeys: Schema3.optional(Schema3.Array(EpochKeyInputSchema))
|
|
233
|
+
});
|
|
234
|
+
function resolveRuntimeConfig(source) {
|
|
235
|
+
return Schema3.decodeUnknownEffect(RuntimeConfigSchema)(source).pipe(Effect.map((config) => ({
|
|
236
|
+
relays: [...config.relays],
|
|
237
|
+
privateKey: config.privateKey,
|
|
238
|
+
epochKeys: config.epochKeys?.map((ek) => ({ epochId: EpochId(ek.epochId), key: ek.key })),
|
|
239
|
+
dbName: DatabaseName(config.dbName ?? "tablinum")
|
|
240
|
+
})), Effect.mapError((error) => new ValidationError({
|
|
241
|
+
message: `Invalid Tablinum configuration: ${error.message}`
|
|
242
|
+
})));
|
|
119
243
|
}
|
|
120
244
|
|
|
121
|
-
// src/
|
|
122
|
-
import {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
function storeName(collection) {
|
|
126
|
-
return `col_${collection}`;
|
|
245
|
+
// src/services/Config.ts
|
|
246
|
+
import { ServiceMap } from "effect";
|
|
247
|
+
|
|
248
|
+
class Config extends ServiceMap.Service()("tablinum/Config") {
|
|
127
249
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
let hash = 1;
|
|
134
|
-
for (let i = 0;i < sig.length; i++) {
|
|
135
|
-
hash = hash * 31 + sig.charCodeAt(i) | 0;
|
|
136
|
-
}
|
|
137
|
-
return Math.abs(hash) + 1;
|
|
250
|
+
|
|
251
|
+
// src/services/Tablinum.ts
|
|
252
|
+
import { ServiceMap as ServiceMap2 } from "effect";
|
|
253
|
+
|
|
254
|
+
class Tablinum extends ServiceMap2.Service()("tablinum/Tablinum") {
|
|
138
255
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
256
|
+
|
|
257
|
+
// src/layers/TablinumLive.ts
|
|
258
|
+
import { Effect as Effect21, Exit, Layer as Layer8, Option as Option9, PubSub as PubSub2, Ref as Ref5, Scope as Scope4 } from "effect";
|
|
259
|
+
|
|
260
|
+
// src/crud/watch.ts
|
|
261
|
+
import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
|
|
262
|
+
function watchCollection(ctx, storage, collectionName, filter, mapRecord) {
|
|
263
|
+
const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
|
|
264
|
+
const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
|
|
265
|
+
return mapRecord ? filtered.map(mapRecord) : filtered;
|
|
146
266
|
});
|
|
267
|
+
const changes = Stream.fromPubSub(ctx.pubsub).pipe(Stream.filter((event) => event.collection === collectionName), Stream.mapEffect(() => Effect2.gen(function* () {
|
|
268
|
+
const replaying = yield* Ref.get(ctx.replayingRef);
|
|
269
|
+
if (replaying)
|
|
270
|
+
return;
|
|
271
|
+
return yield* query();
|
|
272
|
+
})), Stream.filter((result) => result !== undefined));
|
|
273
|
+
return Stream.unwrap(Effect2.gen(function* () {
|
|
274
|
+
yield* Effect2.sleep(0);
|
|
275
|
+
const initial = yield* query();
|
|
276
|
+
return Stream.concat(Stream.make(initial), changes);
|
|
277
|
+
}));
|
|
147
278
|
}
|
|
148
|
-
function
|
|
279
|
+
function notifyChange(ctx, event) {
|
|
280
|
+
return PubSub.publish(ctx.pubsub, event).pipe(Effect2.asVoid);
|
|
281
|
+
}
|
|
282
|
+
function notifyReplayComplete(ctx, collections) {
|
|
149
283
|
return Effect2.gen(function* () {
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
events.createIndex("by-record", ["collection", "recordId"]);
|
|
160
|
-
}
|
|
161
|
-
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
162
|
-
database.createObjectStore("giftwraps", {
|
|
163
|
-
keyPath: "id"
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
if (database.objectStoreNames.contains("records")) {
|
|
167
|
-
database.deleteObjectStore("records");
|
|
168
|
-
}
|
|
169
|
-
const expectedStores = new Set;
|
|
170
|
-
for (const [, def] of Object.entries(schema)) {
|
|
171
|
-
const sn = storeName(def.name);
|
|
172
|
-
expectedStores.add(sn);
|
|
173
|
-
if (!database.objectStoreNames.contains(sn)) {
|
|
174
|
-
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
175
|
-
for (const idx of def.indices ?? []) {
|
|
176
|
-
store.createIndex(idx, idx);
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
const tx = database.transaction;
|
|
180
|
-
const store = tx.objectStore(sn);
|
|
181
|
-
const existingIndices = new Set(Array.from(store.indexNames));
|
|
182
|
-
const wantedIndices = new Set(def.indices ?? []);
|
|
183
|
-
for (const idx of existingIndices) {
|
|
184
|
-
if (!wantedIndices.has(idx)) {
|
|
185
|
-
store.deleteIndex(idx);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
for (const idx of wantedIndices) {
|
|
189
|
-
if (!existingIndices.has(idx)) {
|
|
190
|
-
store.createIndex(idx, idx);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const allStores = Array.from(database.objectStoreNames);
|
|
196
|
-
for (const existing of allStores) {
|
|
197
|
-
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
198
|
-
database.deleteObjectStore(existing);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}),
|
|
203
|
-
catch: (e) => new StorageError({
|
|
204
|
-
message: "Failed to open IndexedDB",
|
|
205
|
-
cause: e
|
|
206
|
-
})
|
|
207
|
-
});
|
|
208
|
-
yield* Effect2.addFinalizer(() => Effect2.sync(() => db.close()));
|
|
209
|
-
const handle = {
|
|
210
|
-
putRecord: (collection, record) => wrap("putRecord", () => db.put(storeName(collection), record).then(() => {
|
|
211
|
-
return;
|
|
212
|
-
})),
|
|
213
|
-
getRecord: (collection, id) => wrap("getRecord", () => db.get(storeName(collection), id)),
|
|
214
|
-
getAllRecords: (collection) => wrap("getAllRecords", () => db.getAll(storeName(collection))),
|
|
215
|
-
countRecords: (collection) => wrap("countRecords", () => db.count(storeName(collection))),
|
|
216
|
-
clearRecords: (collection) => wrap("clearRecords", () => db.clear(storeName(collection))),
|
|
217
|
-
getByIndex: (collection, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection), indexName, value)),
|
|
218
|
-
getByIndexRange: (collection, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection), indexName, range)),
|
|
219
|
-
getAllSorted: (collection, indexName, direction) => wrap("getAllSorted", async () => {
|
|
220
|
-
const sn = storeName(collection);
|
|
221
|
-
const tx = db.transaction(sn, "readonly");
|
|
222
|
-
const store = tx.objectStore(sn);
|
|
223
|
-
const index = store.index(indexName);
|
|
224
|
-
const results = [];
|
|
225
|
-
let cursor = await index.openCursor(null, direction ?? "next");
|
|
226
|
-
while (cursor) {
|
|
227
|
-
results.push(cursor.value);
|
|
228
|
-
cursor = await cursor.continue();
|
|
229
|
-
}
|
|
230
|
-
return results;
|
|
231
|
-
}),
|
|
232
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => {
|
|
233
|
-
return;
|
|
234
|
-
})),
|
|
235
|
-
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
236
|
-
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
237
|
-
getEventsByRecord: (collection, recordId) => wrap("getEventsByRecord", () => db.getAllFromIndex("events", "by-record", [collection, recordId])),
|
|
238
|
-
putGiftWrap: (gw) => wrap("putGiftWrap", () => db.put("giftwraps", gw).then(() => {
|
|
239
|
-
return;
|
|
240
|
-
})),
|
|
241
|
-
getGiftWrap: (id) => wrap("getGiftWrap", () => db.get("giftwraps", id)),
|
|
242
|
-
getAllGiftWraps: () => wrap("getAllGiftWraps", () => db.getAll("giftwraps")),
|
|
243
|
-
close: () => Effect2.sync(() => db.close())
|
|
244
|
-
};
|
|
245
|
-
return handle;
|
|
284
|
+
yield* Ref.set(ctx.replayingRef, false);
|
|
285
|
+
for (const collection2 of collections) {
|
|
286
|
+
yield* notifyChange(ctx, {
|
|
287
|
+
collection: collection2,
|
|
288
|
+
recordId: "",
|
|
289
|
+
kind: "update"
|
|
290
|
+
});
|
|
291
|
+
}
|
|
246
292
|
});
|
|
247
293
|
}
|
|
248
294
|
|
|
@@ -260,30 +306,78 @@ function resolveWinner(existing, incoming) {
|
|
|
260
306
|
return incoming.id < existing.id ? incoming : existing;
|
|
261
307
|
}
|
|
262
308
|
|
|
309
|
+
// src/utils/diff.ts
|
|
310
|
+
function isPlainObject(value) {
|
|
311
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
312
|
+
}
|
|
313
|
+
function deepDiff(before, after) {
|
|
314
|
+
const result = {};
|
|
315
|
+
let hasChanges = false;
|
|
316
|
+
for (const key of Object.keys(after)) {
|
|
317
|
+
const a = before[key];
|
|
318
|
+
const b = after[key];
|
|
319
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
320
|
+
const nested = deepDiff(a, b);
|
|
321
|
+
if (nested !== null) {
|
|
322
|
+
result[key] = nested;
|
|
323
|
+
hasChanges = true;
|
|
324
|
+
}
|
|
325
|
+
} else if (Array.isArray(a) && Array.isArray(b)) {
|
|
326
|
+
if (JSON.stringify(a) !== JSON.stringify(b)) {
|
|
327
|
+
result[key] = b;
|
|
328
|
+
hasChanges = true;
|
|
329
|
+
}
|
|
330
|
+
} else if (a !== b) {
|
|
331
|
+
result[key] = b;
|
|
332
|
+
hasChanges = true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return hasChanges ? result : null;
|
|
336
|
+
}
|
|
337
|
+
function deepMerge(target, source) {
|
|
338
|
+
const result = { ...target };
|
|
339
|
+
for (const key of Object.keys(source)) {
|
|
340
|
+
const t = target[key];
|
|
341
|
+
const s = source[key];
|
|
342
|
+
if (isPlainObject(t) && isPlainObject(s)) {
|
|
343
|
+
result[key] = deepMerge(t, s);
|
|
344
|
+
} else {
|
|
345
|
+
result[key] = s;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
263
351
|
// src/storage/records-store.ts
|
|
264
352
|
function buildRecord(event) {
|
|
265
353
|
return {
|
|
266
354
|
id: event.recordId,
|
|
267
|
-
|
|
268
|
-
|
|
355
|
+
_d: event.kind === "d",
|
|
356
|
+
_u: event.createdAt,
|
|
357
|
+
_e: event.id,
|
|
358
|
+
_a: event.author,
|
|
269
359
|
...event.data ?? {}
|
|
270
360
|
};
|
|
271
361
|
}
|
|
272
362
|
function applyEvent(storage, event) {
|
|
273
363
|
return Effect3.gen(function* () {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
364
|
+
const existing = yield* storage.getRecord(event.collection, event.recordId);
|
|
365
|
+
if (existing) {
|
|
366
|
+
const existingMeta = {
|
|
367
|
+
id: existing._e,
|
|
368
|
+
createdAt: existing._u
|
|
369
|
+
};
|
|
370
|
+
const incomingMeta = { id: event.id, createdAt: event.createdAt };
|
|
371
|
+
const winner = resolveWinner(existingMeta, incomingMeta);
|
|
372
|
+
if (winner.id !== event.id)
|
|
373
|
+
return false;
|
|
280
374
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
375
|
+
if (existing && event.kind === "u") {
|
|
376
|
+
yield* storage.putRecord(event.collection, deepMerge(existing, buildRecord(event)));
|
|
377
|
+
} else {
|
|
284
378
|
yield* storage.putRecord(event.collection, buildRecord(event));
|
|
285
379
|
}
|
|
286
|
-
return
|
|
380
|
+
return true;
|
|
287
381
|
});
|
|
288
382
|
}
|
|
289
383
|
function rebuild(storage, collections) {
|
|
@@ -292,76 +386,125 @@ function rebuild(storage, collections) {
|
|
|
292
386
|
yield* storage.clearRecords(col);
|
|
293
387
|
}
|
|
294
388
|
const allEvents = yield* storage.getAllEvents();
|
|
295
|
-
const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt);
|
|
296
|
-
const winners = new Map;
|
|
389
|
+
const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
|
|
297
390
|
for (const event of sorted) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
for (const event of winners.values()) {
|
|
303
|
-
yield* storage.putRecord(event.collection, buildRecord(event));
|
|
391
|
+
if (event.data === null && event.kind !== "d")
|
|
392
|
+
continue;
|
|
393
|
+
yield* applyEvent(storage, event);
|
|
304
394
|
}
|
|
305
395
|
});
|
|
306
396
|
}
|
|
307
397
|
|
|
308
|
-
// src/
|
|
309
|
-
import { Effect as
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
398
|
+
// src/schema/validate.ts
|
|
399
|
+
import { Effect as Effect4, Schema as Schema4 } from "effect";
|
|
400
|
+
function fieldDefToSchema(fd) {
|
|
401
|
+
let base;
|
|
402
|
+
switch (fd.kind) {
|
|
403
|
+
case "string":
|
|
404
|
+
base = Schema4.String;
|
|
405
|
+
break;
|
|
406
|
+
case "number":
|
|
407
|
+
base = Schema4.Number;
|
|
408
|
+
break;
|
|
409
|
+
case "boolean":
|
|
410
|
+
base = Schema4.Boolean;
|
|
411
|
+
break;
|
|
412
|
+
case "json":
|
|
413
|
+
base = Schema4.Unknown;
|
|
414
|
+
break;
|
|
415
|
+
case "object": {
|
|
416
|
+
const nested = {};
|
|
417
|
+
for (const [k, v] of Object.entries(fd.fields)) {
|
|
418
|
+
nested[k] = fieldDefToSchema(v);
|
|
419
|
+
}
|
|
420
|
+
base = Schema4.Struct(nested);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (fd.isArray) {
|
|
425
|
+
base = Schema4.Array(base);
|
|
426
|
+
}
|
|
427
|
+
if (fd.isOptional) {
|
|
428
|
+
base = Schema4.UndefinedOr(base);
|
|
429
|
+
}
|
|
430
|
+
return base;
|
|
326
431
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const replaying = yield* Ref.get(ctx.replayingRef);
|
|
338
|
-
if (replaying)
|
|
339
|
-
return;
|
|
340
|
-
return yield* query();
|
|
341
|
-
})), Stream.filter((result) => result !== undefined));
|
|
342
|
-
return Stream.unwrap(Effect4.gen(function* () {
|
|
343
|
-
const initial = yield* query();
|
|
344
|
-
return Stream.concat(Stream.make(initial), changes);
|
|
345
|
-
}));
|
|
432
|
+
function buildStructSchema(def, options = {}) {
|
|
433
|
+
const schemaFields = {};
|
|
434
|
+
if (options.includeId) {
|
|
435
|
+
schemaFields.id = Schema4.String;
|
|
436
|
+
}
|
|
437
|
+
for (const [name, fieldDef] of Object.entries(def.fields)) {
|
|
438
|
+
const fieldSchema = fieldDefToSchema(fieldDef);
|
|
439
|
+
schemaFields[name] = options.allOptional ? Schema4.optionalKey(fieldSchema) : fieldSchema;
|
|
440
|
+
}
|
|
441
|
+
return Schema4.Struct(schemaFields);
|
|
346
442
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
443
|
+
function buildValidator(collectionName, def) {
|
|
444
|
+
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
445
|
+
return (input) => decode(input).pipe(Effect4.map((result) => result), Effect4.mapError((e) => new ValidationError({
|
|
446
|
+
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
447
|
+
})));
|
|
349
448
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
yield*
|
|
355
|
-
collection
|
|
356
|
-
|
|
357
|
-
|
|
449
|
+
function buildPartialValidator(collectionName, def) {
|
|
450
|
+
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
451
|
+
return (input) => Effect4.gen(function* () {
|
|
452
|
+
if (typeof input !== "object" || input === null) {
|
|
453
|
+
return yield* new ValidationError({
|
|
454
|
+
message: `Validation failed for collection "${collectionName}": expected an object`
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const record = input;
|
|
458
|
+
const unknownField = Object.keys(record).find((key) => !Object.hasOwn(def.fields, key));
|
|
459
|
+
if (unknownField !== undefined) {
|
|
460
|
+
return yield* new ValidationError({
|
|
461
|
+
message: `Unknown field "${unknownField}" in collection "${collectionName}"`,
|
|
462
|
+
field: unknownField
|
|
358
463
|
});
|
|
359
464
|
}
|
|
465
|
+
return yield* decode(record).pipe(Effect4.map((result) => result), Effect4.mapError((e) => new ValidationError({
|
|
466
|
+
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
467
|
+
})));
|
|
360
468
|
});
|
|
361
469
|
}
|
|
362
470
|
|
|
471
|
+
// src/crud/collection-handle.ts
|
|
472
|
+
import { Effect as Effect6, Option as Option3 } from "effect";
|
|
473
|
+
|
|
474
|
+
// src/utils/uuid.ts
|
|
475
|
+
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
476
|
+
function toBase64url(bytes) {
|
|
477
|
+
let result = "";
|
|
478
|
+
for (let i = 0;i < bytes.length; i += 3) {
|
|
479
|
+
const b0 = bytes[i];
|
|
480
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
481
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
482
|
+
result += alphabet[b0 >> 2];
|
|
483
|
+
result += alphabet[(b0 & 3) << 4 | b1 >> 4];
|
|
484
|
+
if (i + 1 < bytes.length)
|
|
485
|
+
result += alphabet[(b1 & 15) << 2 | b2 >> 6];
|
|
486
|
+
if (i + 2 < bytes.length)
|
|
487
|
+
result += alphabet[b2 & 63];
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
function uuidv7() {
|
|
492
|
+
const now = Date.now();
|
|
493
|
+
const bytes = new Uint8Array(16);
|
|
494
|
+
crypto.getRandomValues(bytes);
|
|
495
|
+
bytes[0] = now / 2 ** 40 & 255;
|
|
496
|
+
bytes[1] = now / 2 ** 32 & 255;
|
|
497
|
+
bytes[2] = now / 2 ** 24 & 255;
|
|
498
|
+
bytes[3] = now / 2 ** 16 & 255;
|
|
499
|
+
bytes[4] = now / 2 ** 8 & 255;
|
|
500
|
+
bytes[5] = now & 255;
|
|
501
|
+
bytes[6] = bytes[6] & 15 | 112;
|
|
502
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
503
|
+
return toBase64url(bytes);
|
|
504
|
+
}
|
|
505
|
+
|
|
363
506
|
// src/crud/query-builder.ts
|
|
364
|
-
import { Effect as Effect5, Ref as Ref2, Stream as Stream2 } from "effect";
|
|
507
|
+
import { Effect as Effect5, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
|
|
365
508
|
function emptyPlan() {
|
|
366
509
|
return { filters: [] };
|
|
367
510
|
}
|
|
@@ -375,7 +518,7 @@ function executeQuery(ctx, plan) {
|
|
|
375
518
|
field: plan.fieldName
|
|
376
519
|
});
|
|
377
520
|
}
|
|
378
|
-
if (fieldDef.kind === "json" || fieldDef.isArray) {
|
|
521
|
+
if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
|
|
379
522
|
return yield* new ValidationError({
|
|
380
523
|
message: `Field "${plan.fieldName}" does not support filtering (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`,
|
|
381
524
|
field: plan.fieldName
|
|
@@ -400,17 +543,17 @@ function executeQuery(ctx, plan) {
|
|
|
400
543
|
} else {
|
|
401
544
|
results = [...yield* ctx.storage.getAllRecords(ctx.collectionName)];
|
|
402
545
|
}
|
|
403
|
-
results = results.filter((r) => !r.
|
|
546
|
+
results = results.filter((r) => !r._d);
|
|
404
547
|
for (const f of plan.filters) {
|
|
405
548
|
results = results.filter(f);
|
|
406
549
|
}
|
|
407
550
|
if (plan.orderBy) {
|
|
408
551
|
const alreadySorted = ctx.def.indices.includes(plan.orderBy.field) && plan.filters.length === 0 && !plan.indexQuery;
|
|
409
552
|
if (!alreadySorted) {
|
|
410
|
-
const { field, direction } = plan.orderBy;
|
|
553
|
+
const { field: field2, direction } = plan.orderBy;
|
|
411
554
|
results = results.sort((a, b) => {
|
|
412
|
-
const va = a[
|
|
413
|
-
const vb = b[
|
|
555
|
+
const va = a[field2];
|
|
556
|
+
const vb = b[field2];
|
|
414
557
|
const cmp = va < vb ? -1 : va > vb ? 1 : 0;
|
|
415
558
|
return direction === "desc" ? -cmp : cmp;
|
|
416
559
|
});
|
|
@@ -444,9 +587,9 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
444
587
|
...plan,
|
|
445
588
|
filters: [...plan.filters, (r) => fn(ctx.mapRecord(r))]
|
|
446
589
|
}),
|
|
447
|
-
sortBy: (
|
|
590
|
+
sortBy: (field2) => makeQueryBuilder(ctx, {
|
|
448
591
|
...plan,
|
|
449
|
-
orderBy: { field, direction: plan.orderBy?.direction ?? "asc" }
|
|
592
|
+
orderBy: { field: field2, direction: plan.orderBy?.direction ?? "asc" }
|
|
450
593
|
}),
|
|
451
594
|
reverse: () => makeQueryBuilder(ctx, {
|
|
452
595
|
...plan,
|
|
@@ -458,39 +601,8 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
458
601
|
offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
|
|
459
602
|
limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
|
|
460
603
|
get: () => executeQuery(ctx, plan),
|
|
461
|
-
first: () => Effect5.
|
|
462
|
-
|
|
463
|
-
const results = yield* executeQuery(ctx, limitedPlan);
|
|
464
|
-
return results[0] ?? null;
|
|
465
|
-
}),
|
|
466
|
-
count: () => Effect5.gen(function* () {
|
|
467
|
-
const results = yield* executeQuery(ctx, plan);
|
|
468
|
-
return results.length;
|
|
469
|
-
}),
|
|
470
|
-
watch: () => watchQuery(ctx, plan)
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
function makeOrderByBuilder(ctx, plan) {
|
|
474
|
-
return {
|
|
475
|
-
reverse: () => makeOrderByBuilder(ctx, {
|
|
476
|
-
...plan,
|
|
477
|
-
orderBy: {
|
|
478
|
-
field: plan.orderBy.field,
|
|
479
|
-
direction: plan.orderBy.direction === "desc" ? "asc" : "desc"
|
|
480
|
-
}
|
|
481
|
-
}),
|
|
482
|
-
offset: (n) => makeOrderByBuilder(ctx, { ...plan, offset: n }),
|
|
483
|
-
limit: (n) => makeOrderByBuilder(ctx, { ...plan, limit: n }),
|
|
484
|
-
get: () => executeQuery(ctx, plan),
|
|
485
|
-
first: () => Effect5.gen(function* () {
|
|
486
|
-
const limitedPlan = { ...plan, limit: 1 };
|
|
487
|
-
const results = yield* executeQuery(ctx, limitedPlan);
|
|
488
|
-
return results[0] ?? null;
|
|
489
|
-
}),
|
|
490
|
-
count: () => Effect5.gen(function* () {
|
|
491
|
-
const results = yield* executeQuery(ctx, plan);
|
|
492
|
-
return results.length;
|
|
493
|
-
}),
|
|
604
|
+
first: () => Effect5.map(executeQuery(ctx, { ...plan, limit: 1 }), (results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()),
|
|
605
|
+
count: () => Effect5.map(executeQuery(ctx, plan), (results) => results.length),
|
|
494
606
|
watch: () => watchQuery(ctx, plan)
|
|
495
607
|
};
|
|
496
608
|
}
|
|
@@ -548,16 +660,76 @@ function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName,
|
|
|
548
660
|
...emptyPlan(),
|
|
549
661
|
orderBy: { field: fieldName, direction: "asc" }
|
|
550
662
|
};
|
|
551
|
-
return
|
|
663
|
+
return makeQueryBuilder(ctx, plan);
|
|
552
664
|
}
|
|
553
665
|
|
|
554
666
|
// src/crud/collection-handle.ts
|
|
667
|
+
var KIND_FULL = { c: "create", u: "update", d: "delete" };
|
|
668
|
+
function sortChronologically(events) {
|
|
669
|
+
return [...events].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
|
|
670
|
+
}
|
|
671
|
+
function replayState(recordId, events, stopAtId) {
|
|
672
|
+
let state = null;
|
|
673
|
+
for (const e of events) {
|
|
674
|
+
if (e.kind === "d") {
|
|
675
|
+
state = null;
|
|
676
|
+
} else if (e.data !== null) {
|
|
677
|
+
if (state === null) {
|
|
678
|
+
state = { id: recordId, ...e.data };
|
|
679
|
+
} else {
|
|
680
|
+
state = deepMerge(state, e.data);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (stopAtId !== undefined && e.id === stopAtId) {
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return state;
|
|
688
|
+
}
|
|
689
|
+
function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
690
|
+
return Effect6.gen(function* () {
|
|
691
|
+
const chronological = sortChronologically(allSorted);
|
|
692
|
+
const state = replayState(recordId, chronological, target.id);
|
|
693
|
+
if (state) {
|
|
694
|
+
yield* storage.putEvent({ ...target, data: state });
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function pruneEvents(storage, collection2, recordId, retention) {
|
|
699
|
+
return Effect6.gen(function* () {
|
|
700
|
+
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
701
|
+
if (events.length <= retention)
|
|
702
|
+
return;
|
|
703
|
+
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
704
|
+
const retained = sorted.slice(0, retention);
|
|
705
|
+
const toStrip = sorted.slice(retention);
|
|
706
|
+
const oldestRetained = retained[retained.length - 1];
|
|
707
|
+
if (retention > 0 && oldestRetained?.kind === "u" && oldestRetained.data !== null) {
|
|
708
|
+
yield* promoteToSnapshot(storage, collection2, recordId, oldestRetained, sorted);
|
|
709
|
+
}
|
|
710
|
+
for (const e of toStrip) {
|
|
711
|
+
if (e.data !== null)
|
|
712
|
+
yield* storage.stripEventData(e.id);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
555
716
|
function mapRecord(record) {
|
|
556
|
-
const {
|
|
717
|
+
const { _d, _u, _a, _e, ...fields } = record;
|
|
557
718
|
return fields;
|
|
558
719
|
}
|
|
559
|
-
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, onWrite) {
|
|
720
|
+
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite) {
|
|
560
721
|
const collectionName = def.name;
|
|
722
|
+
const commitEvent = (event) => Effect6.gen(function* () {
|
|
723
|
+
yield* storage.putEvent(event);
|
|
724
|
+
yield* applyEvent(storage, event);
|
|
725
|
+
if (onWrite)
|
|
726
|
+
yield* onWrite(event);
|
|
727
|
+
yield* notifyChange(watchCtx, {
|
|
728
|
+
collection: collectionName,
|
|
729
|
+
recordId: event.recordId,
|
|
730
|
+
kind: KIND_FULL[event.kind]
|
|
731
|
+
});
|
|
732
|
+
});
|
|
561
733
|
const handle = {
|
|
562
734
|
add: (data) => Effect6.gen(function* () {
|
|
563
735
|
const id = uuidv7();
|
|
@@ -567,54 +739,42 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
567
739
|
id: makeEventId(),
|
|
568
740
|
collection: collectionName,
|
|
569
741
|
recordId: id,
|
|
570
|
-
kind: "
|
|
742
|
+
kind: "c",
|
|
571
743
|
data: fullRecord,
|
|
572
|
-
createdAt: Date.now()
|
|
744
|
+
createdAt: Date.now(),
|
|
745
|
+
author: localAuthor
|
|
573
746
|
};
|
|
574
|
-
yield*
|
|
575
|
-
yield* applyEvent(storage, event);
|
|
576
|
-
if (onWrite)
|
|
577
|
-
yield* onWrite(event);
|
|
578
|
-
yield* notifyChange(watchCtx, {
|
|
579
|
-
collection: collectionName,
|
|
580
|
-
recordId: id,
|
|
581
|
-
kind: "create"
|
|
582
|
-
});
|
|
747
|
+
yield* commitEvent(event);
|
|
583
748
|
return id;
|
|
584
749
|
}),
|
|
585
750
|
update: (id, data) => Effect6.gen(function* () {
|
|
586
751
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
587
|
-
if (!existing || existing.
|
|
752
|
+
if (!existing || existing._d) {
|
|
588
753
|
return yield* new NotFoundError({
|
|
589
754
|
collection: collectionName,
|
|
590
755
|
id
|
|
591
756
|
});
|
|
592
757
|
}
|
|
593
758
|
yield* partialValidator(data);
|
|
594
|
-
const {
|
|
759
|
+
const { _d, _u, _a, _e, ...existingFields } = existing;
|
|
595
760
|
const merged = { ...existingFields, ...data, id };
|
|
596
761
|
yield* validator(merged);
|
|
762
|
+
const diff = deepDiff(existingFields, merged);
|
|
597
763
|
const event = {
|
|
598
764
|
id: makeEventId(),
|
|
599
765
|
collection: collectionName,
|
|
600
766
|
recordId: id,
|
|
601
|
-
kind: "
|
|
602
|
-
data:
|
|
603
|
-
createdAt: Date.now()
|
|
767
|
+
kind: "u",
|
|
768
|
+
data: diff ?? { id },
|
|
769
|
+
createdAt: Date.now(),
|
|
770
|
+
author: localAuthor
|
|
604
771
|
};
|
|
605
|
-
yield*
|
|
606
|
-
yield*
|
|
607
|
-
if (onWrite)
|
|
608
|
-
yield* onWrite(event);
|
|
609
|
-
yield* notifyChange(watchCtx, {
|
|
610
|
-
collection: collectionName,
|
|
611
|
-
recordId: id,
|
|
612
|
-
kind: "update"
|
|
613
|
-
});
|
|
772
|
+
yield* commitEvent(event);
|
|
773
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
614
774
|
}),
|
|
615
775
|
delete: (id) => Effect6.gen(function* () {
|
|
616
776
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
617
|
-
if (!existing || existing.
|
|
777
|
+
if (!existing || existing._d) {
|
|
618
778
|
return yield* new NotFoundError({
|
|
619
779
|
collection: collectionName,
|
|
620
780
|
id
|
|
@@ -624,23 +784,42 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
624
784
|
id: makeEventId(),
|
|
625
785
|
collection: collectionName,
|
|
626
786
|
recordId: id,
|
|
627
|
-
kind: "
|
|
787
|
+
kind: "d",
|
|
628
788
|
data: null,
|
|
629
|
-
createdAt: Date.now()
|
|
789
|
+
createdAt: Date.now(),
|
|
790
|
+
author: localAuthor
|
|
630
791
|
};
|
|
631
|
-
yield*
|
|
632
|
-
yield*
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
yield*
|
|
792
|
+
yield* commitEvent(event);
|
|
793
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
794
|
+
}),
|
|
795
|
+
undo: (id) => Effect6.gen(function* () {
|
|
796
|
+
const existing = yield* storage.getRecord(collectionName, id);
|
|
797
|
+
if (!existing) {
|
|
798
|
+
return yield* new NotFoundError({ collection: collectionName, id });
|
|
799
|
+
}
|
|
800
|
+
const events = sortChronologically(yield* storage.getEventsByRecord(collectionName, id));
|
|
801
|
+
if (events.length < 2) {
|
|
802
|
+
return yield* new NotFoundError({ collection: collectionName, id });
|
|
803
|
+
}
|
|
804
|
+
const state = replayState(id, events.slice(0, -1));
|
|
805
|
+
if (!state) {
|
|
806
|
+
return yield* new NotFoundError({ collection: collectionName, id });
|
|
807
|
+
}
|
|
808
|
+
const event = {
|
|
809
|
+
id: makeEventId(),
|
|
636
810
|
collection: collectionName,
|
|
637
811
|
recordId: id,
|
|
638
|
-
kind: "
|
|
639
|
-
|
|
812
|
+
kind: "u",
|
|
813
|
+
data: state,
|
|
814
|
+
createdAt: Date.now(),
|
|
815
|
+
author: localAuthor
|
|
816
|
+
};
|
|
817
|
+
yield* commitEvent(event);
|
|
818
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
640
819
|
}),
|
|
641
820
|
get: (id) => Effect6.gen(function* () {
|
|
642
821
|
const record = yield* storage.getRecord(collectionName, id);
|
|
643
|
-
if (!record || record.
|
|
822
|
+
if (!record || record._d) {
|
|
644
823
|
return yield* new NotFoundError({
|
|
645
824
|
collection: collectionName,
|
|
646
825
|
id
|
|
@@ -648,15 +827,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
648
827
|
}
|
|
649
828
|
return mapRecord(record);
|
|
650
829
|
}),
|
|
651
|
-
first: () => Effect6.
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
return found ? mapRecord(found) : null;
|
|
655
|
-
}),
|
|
656
|
-
count: () => Effect6.gen(function* () {
|
|
657
|
-
const all = yield* storage.getAllRecords(collectionName);
|
|
658
|
-
return all.filter((r) => !r._deleted).length;
|
|
830
|
+
first: () => Effect6.map(storage.getAllRecords(collectionName), (all) => {
|
|
831
|
+
const found = all.find((r) => !r._d);
|
|
832
|
+
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
659
833
|
}),
|
|
834
|
+
count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
660
835
|
watch: () => watchCollection(watchCtx, storage, collectionName, undefined, mapRecord),
|
|
661
836
|
where: (fieldName) => createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord),
|
|
662
837
|
orderBy: (fieldName) => createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord)
|
|
@@ -664,306 +839,13 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
664
839
|
return handle;
|
|
665
840
|
}
|
|
666
841
|
|
|
667
|
-
// src/db/identity.ts
|
|
668
|
-
import { Effect as Effect7 } from "effect";
|
|
669
|
-
import { getPublicKey } from "nostr-tools/pure";
|
|
670
|
-
function bytesToHex(bytes) {
|
|
671
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
672
|
-
}
|
|
673
|
-
function createIdentity(suppliedKey) {
|
|
674
|
-
return Effect7.gen(function* () {
|
|
675
|
-
let privateKey;
|
|
676
|
-
if (suppliedKey) {
|
|
677
|
-
if (suppliedKey.length !== 32) {
|
|
678
|
-
return yield* new CryptoError({
|
|
679
|
-
message: `Private key must be 32 bytes, got ${suppliedKey.length}`
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
privateKey = suppliedKey;
|
|
683
|
-
} else {
|
|
684
|
-
privateKey = new Uint8Array(32);
|
|
685
|
-
crypto.getRandomValues(privateKey);
|
|
686
|
-
}
|
|
687
|
-
const privateKeyHex = bytesToHex(privateKey);
|
|
688
|
-
let publicKey;
|
|
689
|
-
try {
|
|
690
|
-
publicKey = getPublicKey(privateKey);
|
|
691
|
-
} catch (e) {
|
|
692
|
-
return yield* new CryptoError({
|
|
693
|
-
message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
|
|
694
|
-
cause: e
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
return {
|
|
698
|
-
privateKey,
|
|
699
|
-
publicKey,
|
|
700
|
-
exportKey: () => privateKeyHex
|
|
701
|
-
};
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// src/sync/gift-wrap.ts
|
|
706
|
-
import { Effect as Effect8 } from "effect";
|
|
707
|
-
import { wrapEvent, unwrapEvent } from "nostr-tools/nip59";
|
|
708
|
-
function createGiftWrapHandle(privateKey, publicKey) {
|
|
709
|
-
return {
|
|
710
|
-
wrap: (rumor) => Effect8.try({
|
|
711
|
-
try: () => wrapEvent(rumor, privateKey, publicKey),
|
|
712
|
-
catch: (e) => new CryptoError({
|
|
713
|
-
message: `Gift wrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
714
|
-
cause: e
|
|
715
|
-
})
|
|
716
|
-
}),
|
|
717
|
-
unwrap: (giftWrap) => Effect8.try({
|
|
718
|
-
try: () => unwrapEvent(giftWrap, privateKey),
|
|
719
|
-
catch: (e) => new CryptoError({
|
|
720
|
-
message: `Gift unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
721
|
-
cause: e
|
|
722
|
-
})
|
|
723
|
-
})
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// src/sync/relay.ts
|
|
728
|
-
import { Effect as Effect9 } from "effect";
|
|
729
|
-
import { Relay } from "nostr-tools/relay";
|
|
730
|
-
function createRelayHandle() {
|
|
731
|
-
const connections = new Map;
|
|
732
|
-
const getRelay = (url) => Effect9.tryPromise({
|
|
733
|
-
try: async () => {
|
|
734
|
-
const existing = connections.get(url);
|
|
735
|
-
if (existing && existing.connected !== false) {
|
|
736
|
-
return existing;
|
|
737
|
-
}
|
|
738
|
-
connections.delete(url);
|
|
739
|
-
const relay = await Relay.connect(url);
|
|
740
|
-
connections.set(url, relay);
|
|
741
|
-
return relay;
|
|
742
|
-
},
|
|
743
|
-
catch: (e) => new RelayError({
|
|
744
|
-
message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
745
|
-
url,
|
|
746
|
-
cause: e
|
|
747
|
-
})
|
|
748
|
-
});
|
|
749
|
-
return {
|
|
750
|
-
publish: (event, urls) => Effect9.gen(function* () {
|
|
751
|
-
const errors = [];
|
|
752
|
-
for (const url of urls) {
|
|
753
|
-
const result = yield* Effect9.result(Effect9.gen(function* () {
|
|
754
|
-
const relay = yield* getRelay(url);
|
|
755
|
-
yield* Effect9.tryPromise({
|
|
756
|
-
try: () => relay.publish(event),
|
|
757
|
-
catch: (e) => {
|
|
758
|
-
connections.delete(url);
|
|
759
|
-
return new RelayError({
|
|
760
|
-
message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
761
|
-
url,
|
|
762
|
-
cause: e
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
}));
|
|
767
|
-
if (result._tag === "Failure") {
|
|
768
|
-
errors.push({ url, error: result });
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
if (errors.length === urls.length && urls.length > 0) {
|
|
772
|
-
return yield* new RelayError({
|
|
773
|
-
message: `Publish failed on all ${urls.length} relays`
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
}),
|
|
777
|
-
fetchEvents: (ids, url) => Effect9.gen(function* () {
|
|
778
|
-
if (ids.length === 0)
|
|
779
|
-
return [];
|
|
780
|
-
const relay = yield* getRelay(url);
|
|
781
|
-
return yield* Effect9.tryPromise({
|
|
782
|
-
try: () => new Promise((resolve) => {
|
|
783
|
-
const events = [];
|
|
784
|
-
const timer = setTimeout(() => {
|
|
785
|
-
sub.close();
|
|
786
|
-
resolve(events);
|
|
787
|
-
}, 1e4);
|
|
788
|
-
const sub = relay.subscribe([{ ids }], {
|
|
789
|
-
onevent(evt) {
|
|
790
|
-
events.push(evt);
|
|
791
|
-
},
|
|
792
|
-
oneose() {
|
|
793
|
-
clearTimeout(timer);
|
|
794
|
-
sub.close();
|
|
795
|
-
resolve(events);
|
|
796
|
-
}
|
|
797
|
-
});
|
|
798
|
-
}),
|
|
799
|
-
catch: (e) => {
|
|
800
|
-
connections.delete(url);
|
|
801
|
-
return new RelayError({
|
|
802
|
-
message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
803
|
-
url,
|
|
804
|
-
cause: e
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
});
|
|
808
|
-
}),
|
|
809
|
-
fetchByFilter: (filter, url) => Effect9.gen(function* () {
|
|
810
|
-
const relay = yield* getRelay(url);
|
|
811
|
-
return yield* Effect9.tryPromise({
|
|
812
|
-
try: () => new Promise((resolve) => {
|
|
813
|
-
const events = [];
|
|
814
|
-
const timer = setTimeout(() => {
|
|
815
|
-
sub.close();
|
|
816
|
-
resolve(events);
|
|
817
|
-
}, 1e4);
|
|
818
|
-
const sub = relay.subscribe([filter], {
|
|
819
|
-
onevent(evt) {
|
|
820
|
-
events.push(evt);
|
|
821
|
-
},
|
|
822
|
-
oneose() {
|
|
823
|
-
clearTimeout(timer);
|
|
824
|
-
sub.close();
|
|
825
|
-
resolve(events);
|
|
826
|
-
}
|
|
827
|
-
});
|
|
828
|
-
}),
|
|
829
|
-
catch: (e) => {
|
|
830
|
-
connections.delete(url);
|
|
831
|
-
return new RelayError({
|
|
832
|
-
message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
833
|
-
url,
|
|
834
|
-
cause: e
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
});
|
|
838
|
-
}),
|
|
839
|
-
subscribe: (filter, url, onEvent) => Effect9.gen(function* () {
|
|
840
|
-
const relay = yield* getRelay(url);
|
|
841
|
-
relay.subscribe([filter], {
|
|
842
|
-
onevent(evt) {
|
|
843
|
-
onEvent(evt);
|
|
844
|
-
},
|
|
845
|
-
oneose() {}
|
|
846
|
-
});
|
|
847
|
-
}),
|
|
848
|
-
sendNegMsg: (url, subId, filter, msgHex) => Effect9.gen(function* () {
|
|
849
|
-
const relay = yield* getRelay(url);
|
|
850
|
-
return yield* Effect9.tryPromise({
|
|
851
|
-
try: () => new Promise((resolve, reject) => {
|
|
852
|
-
const timer = setTimeout(() => {
|
|
853
|
-
reject(new Error("NIP-77 negotiation timeout"));
|
|
854
|
-
}, 30000);
|
|
855
|
-
const sub = relay.subscribe([filter], {
|
|
856
|
-
onevent() {},
|
|
857
|
-
oneose() {}
|
|
858
|
-
});
|
|
859
|
-
const ws = relay._ws || relay.ws;
|
|
860
|
-
if (!ws) {
|
|
861
|
-
clearTimeout(timer);
|
|
862
|
-
sub.close();
|
|
863
|
-
reject(new Error("Cannot access relay WebSocket"));
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
const handler = (msg) => {
|
|
867
|
-
try {
|
|
868
|
-
const data = JSON.parse(typeof msg.data === "string" ? msg.data : "");
|
|
869
|
-
if (!Array.isArray(data))
|
|
870
|
-
return;
|
|
871
|
-
if (data[0] === "NEG-MSG" && data[1] === subId) {
|
|
872
|
-
clearTimeout(timer);
|
|
873
|
-
sub.close();
|
|
874
|
-
resolve({
|
|
875
|
-
msgHex: data[2],
|
|
876
|
-
haveIds: [],
|
|
877
|
-
needIds: []
|
|
878
|
-
});
|
|
879
|
-
ws.removeEventListener("message", handler);
|
|
880
|
-
} else if (data[0] === "NEG-ERR" && data[1] === subId) {
|
|
881
|
-
clearTimeout(timer);
|
|
882
|
-
sub.close();
|
|
883
|
-
reject(new Error(`NEG-ERR: ${data[2]}`));
|
|
884
|
-
ws.removeEventListener("message", handler);
|
|
885
|
-
}
|
|
886
|
-
} catch {}
|
|
887
|
-
};
|
|
888
|
-
ws.addEventListener("message", handler);
|
|
889
|
-
ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
|
|
890
|
-
}),
|
|
891
|
-
catch: (e) => {
|
|
892
|
-
connections.delete(url);
|
|
893
|
-
return new RelayError({
|
|
894
|
-
message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
895
|
-
url,
|
|
896
|
-
cause: e
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
});
|
|
900
|
-
}),
|
|
901
|
-
closeAll: () => Effect9.sync(() => {
|
|
902
|
-
for (const [url, relay] of connections) {
|
|
903
|
-
relay.close();
|
|
904
|
-
connections.delete(url);
|
|
905
|
-
}
|
|
906
|
-
})
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// src/sync/publish-queue.ts
|
|
911
|
-
import { Effect as Effect10, Ref as Ref3 } from "effect";
|
|
912
|
-
function createPublishQueue(storage, relay) {
|
|
913
|
-
return Effect10.gen(function* () {
|
|
914
|
-
const pendingRef = yield* Ref3.make(new Set);
|
|
915
|
-
return {
|
|
916
|
-
enqueue: (eventId) => Ref3.update(pendingRef, (set) => {
|
|
917
|
-
const next = new Set(set);
|
|
918
|
-
next.add(eventId);
|
|
919
|
-
return next;
|
|
920
|
-
}),
|
|
921
|
-
flush: (relayUrls) => Effect10.gen(function* () {
|
|
922
|
-
const pending = yield* Ref3.get(pendingRef);
|
|
923
|
-
if (pending.size === 0)
|
|
924
|
-
return;
|
|
925
|
-
const succeeded = new Set;
|
|
926
|
-
for (const eventId of pending) {
|
|
927
|
-
const gw = yield* storage.getGiftWrap(eventId);
|
|
928
|
-
if (!gw) {
|
|
929
|
-
succeeded.add(eventId);
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
const result = yield* Effect10.result(relay.publish(gw.event, relayUrls));
|
|
933
|
-
if (result._tag === "Success") {
|
|
934
|
-
succeeded.add(eventId);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
yield* Ref3.update(pendingRef, (set) => {
|
|
938
|
-
const next = new Set(set);
|
|
939
|
-
for (const id of succeeded) {
|
|
940
|
-
next.delete(id);
|
|
941
|
-
}
|
|
942
|
-
return next;
|
|
943
|
-
});
|
|
944
|
-
}),
|
|
945
|
-
size: () => Ref3.get(pendingRef).pipe(Effect10.map((s) => s.size))
|
|
946
|
-
};
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// src/sync/sync-status.ts
|
|
951
|
-
import { Effect as Effect11, SubscriptionRef } from "effect";
|
|
952
|
-
function createSyncStatusHandle() {
|
|
953
|
-
return Effect11.gen(function* () {
|
|
954
|
-
const ref = yield* SubscriptionRef.make("idle");
|
|
955
|
-
return {
|
|
956
|
-
get: () => SubscriptionRef.get(ref),
|
|
957
|
-
set: (status) => SubscriptionRef.set(ref, status)
|
|
958
|
-
};
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
|
|
962
842
|
// src/sync/sync-service.ts
|
|
963
|
-
import { Effect as
|
|
843
|
+
import { Effect as Effect8, Option as Option5, Ref as Ref3, Schedule } from "effect";
|
|
844
|
+
import { unwrapEvent } from "nostr-tools/nip59";
|
|
845
|
+
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
964
846
|
|
|
965
847
|
// src/sync/negentropy.ts
|
|
966
|
-
import { Effect as
|
|
848
|
+
import { Effect as Effect7 } from "effect";
|
|
967
849
|
|
|
968
850
|
// src/vendor/negentropy.js
|
|
969
851
|
var PROTOCOL_VERSION = 97;
|
|
@@ -1483,30 +1365,25 @@ function itemCompare(a, b) {
|
|
|
1483
1365
|
}
|
|
1484
1366
|
|
|
1485
1367
|
// src/sync/negentropy.ts
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
return bytes;
|
|
1492
|
-
}
|
|
1493
|
-
function reconcileWithRelay(storage, relay, relayUrl, publicKey) {
|
|
1494
|
-
return Effect12.gen(function* () {
|
|
1368
|
+
import { hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
|
|
1369
|
+
import { GiftWrap } from "nostr-tools/kinds";
|
|
1370
|
+
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1371
|
+
return Effect7.gen(function* () {
|
|
1495
1372
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1496
1373
|
const storageVector = new NegentropyStorageVector;
|
|
1497
1374
|
for (const gw of allGiftWraps) {
|
|
1498
|
-
storageVector.insert(gw.createdAt,
|
|
1375
|
+
storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
|
|
1499
1376
|
}
|
|
1500
1377
|
storageVector.seal();
|
|
1501
1378
|
const neg = new Negentropy(storageVector, 0);
|
|
1502
1379
|
const filter = {
|
|
1503
|
-
kinds: [
|
|
1504
|
-
"#p": [
|
|
1380
|
+
kinds: [GiftWrap],
|
|
1381
|
+
"#p": Array.isArray(publicKeys) ? publicKeys : [publicKeys]
|
|
1505
1382
|
};
|
|
1506
1383
|
const allHaveIds = [];
|
|
1507
1384
|
const allNeedIds = [];
|
|
1508
1385
|
const subId = `neg-${Date.now()}`;
|
|
1509
|
-
const initialMsg = yield*
|
|
1386
|
+
const initialMsg = yield* Effect7.try({
|
|
1510
1387
|
try: () => neg.initiate(),
|
|
1511
1388
|
catch: (e) => new SyncError({
|
|
1512
1389
|
message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1519,7 +1396,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKey) {
|
|
|
1519
1396
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1520
1397
|
if (response.msgHex === null)
|
|
1521
1398
|
break;
|
|
1522
|
-
const reconcileResult = yield*
|
|
1399
|
+
const reconcileResult = yield* Effect7.try({
|
|
1523
1400
|
try: () => neg.reconcile(response.msgHex),
|
|
1524
1401
|
catch: (e) => new SyncError({
|
|
1525
1402
|
message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1538,512 +1415,1667 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKey) {
|
|
|
1538
1415
|
});
|
|
1539
1416
|
}
|
|
1540
1417
|
|
|
1541
|
-
// src/
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1418
|
+
// src/db/key-rotation.ts
|
|
1419
|
+
import { Option as Option4, Schema as Schema5 } from "effect";
|
|
1420
|
+
import { generateSecretKey } from "nostr-tools/pure";
|
|
1421
|
+
import { wrapEvent } from "nostr-tools/nip59";
|
|
1422
|
+
var HexKeySchema2 = Schema5.String.check(Schema5.isPattern(/^[0-9a-f]{64}$/i));
|
|
1423
|
+
var RotationDataSchema = Schema5.Struct({
|
|
1424
|
+
_rotation: Schema5.Literal(true),
|
|
1425
|
+
epochId: Schema5.String,
|
|
1426
|
+
epochKey: HexKeySchema2,
|
|
1427
|
+
parentEpoch: Schema5.String,
|
|
1428
|
+
removedMembers: Schema5.Array(Schema5.String)
|
|
1429
|
+
});
|
|
1430
|
+
var RemovalNoticeSchema = Schema5.Struct({
|
|
1431
|
+
_removed: Schema5.Literal(true),
|
|
1432
|
+
epochId: Schema5.String,
|
|
1433
|
+
removedBy: Schema5.String
|
|
1434
|
+
});
|
|
1435
|
+
var decodeRotationData = Schema5.decodeUnknownSync(Schema5.fromJsonString(RotationDataSchema));
|
|
1436
|
+
var decodeRemovalNotice = Schema5.decodeUnknownSync(Schema5.fromJsonString(RemovalNoticeSchema));
|
|
1437
|
+
function createRotation(epochStore, senderPrivateKey, senderPublicKey, remainingMemberPubkeys, removedMemberPubkeys) {
|
|
1438
|
+
const newSk = generateSecretKey();
|
|
1439
|
+
const newKeyHex = bytesToHex(newSk);
|
|
1440
|
+
const currentEpoch = getCurrentEpoch(epochStore);
|
|
1441
|
+
const epochId = EpochId(uuidv7());
|
|
1442
|
+
const epoch = createEpochKey(epochId, newKeyHex, senderPublicKey, currentEpoch.id);
|
|
1443
|
+
const rotationData = {
|
|
1444
|
+
_rotation: true,
|
|
1445
|
+
epochId,
|
|
1446
|
+
epochKey: newKeyHex,
|
|
1447
|
+
parentEpoch: currentEpoch.id,
|
|
1448
|
+
removedMembers: removedMemberPubkeys
|
|
1449
|
+
};
|
|
1450
|
+
const rumor = {
|
|
1451
|
+
kind: 1,
|
|
1452
|
+
content: JSON.stringify(rotationData),
|
|
1453
|
+
tags: [["d", `_system:rotation:${epochId}`]],
|
|
1454
|
+
created_at: Math.floor(Date.now() / 1000)
|
|
1455
|
+
};
|
|
1456
|
+
const wrappedEvents = [];
|
|
1457
|
+
for (const memberPubkey of remainingMemberPubkeys) {
|
|
1458
|
+
if (memberPubkey === senderPublicKey)
|
|
1459
|
+
continue;
|
|
1460
|
+
const wrapped = wrapEvent(rumor, senderPrivateKey, memberPubkey);
|
|
1461
|
+
wrappedEvents.push(wrapped);
|
|
1462
|
+
}
|
|
1463
|
+
const removalData = {
|
|
1464
|
+
_removed: true,
|
|
1465
|
+
epochId,
|
|
1466
|
+
removedBy: senderPublicKey
|
|
1467
|
+
};
|
|
1468
|
+
const removalRumor = {
|
|
1469
|
+
kind: 1,
|
|
1470
|
+
content: JSON.stringify(removalData),
|
|
1471
|
+
tags: [["d", `_system:removed:${epochId}`]],
|
|
1472
|
+
created_at: Math.floor(Date.now() / 1000)
|
|
1473
|
+
};
|
|
1474
|
+
const removalNotices = [];
|
|
1475
|
+
for (const removedPubkey of removedMemberPubkeys) {
|
|
1476
|
+
const wrapped = wrapEvent(removalRumor, senderPrivateKey, removedPubkey);
|
|
1477
|
+
removalNotices.push(wrapped);
|
|
1478
|
+
}
|
|
1479
|
+
return { epoch, wrappedEvents, removalNotices };
|
|
1480
|
+
}
|
|
1481
|
+
function parseRotationEvent(content, dTag) {
|
|
1482
|
+
if (!dTag.startsWith("_system:rotation:"))
|
|
1483
|
+
return Option4.none();
|
|
1484
|
+
try {
|
|
1485
|
+
return Option4.some(decodeRotationData(content));
|
|
1486
|
+
} catch {
|
|
1487
|
+
return Option4.none();
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function parseRemovalNotice(content, dTag) {
|
|
1491
|
+
if (!dTag.startsWith("_system:removed:"))
|
|
1492
|
+
return Option4.none();
|
|
1493
|
+
try {
|
|
1494
|
+
return Option4.some(decodeRemovalNotice(content));
|
|
1495
|
+
} catch {
|
|
1496
|
+
return Option4.none();
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/sync/sync-service.ts
|
|
1501
|
+
function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
|
|
1502
|
+
const getSubscriptionPubKeys = () => {
|
|
1503
|
+
return getAllPublicKeys(epochStore);
|
|
1504
|
+
};
|
|
1505
|
+
const notifyCollectionUpdated = (collection2) => notifyChange(watchCtx, {
|
|
1506
|
+
collection: collection2,
|
|
1507
|
+
recordId: "",
|
|
1508
|
+
kind: "create"
|
|
1509
|
+
});
|
|
1510
|
+
const forkHandled = (effect) => {
|
|
1511
|
+
Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.forkIn(scope)));
|
|
1512
|
+
};
|
|
1513
|
+
let autoFlushActive = false;
|
|
1514
|
+
const autoFlushEffect = Effect8.gen(function* () {
|
|
1515
|
+
const size = yield* publishQueue.size();
|
|
1516
|
+
if (size === 0)
|
|
1517
|
+
return;
|
|
1518
|
+
yield* syncStatus.set("syncing");
|
|
1519
|
+
yield* publishQueue.flush(relayUrls);
|
|
1520
|
+
const remaining = yield* publishQueue.size();
|
|
1521
|
+
if (remaining > 0)
|
|
1522
|
+
yield* Effect8.fail("pending");
|
|
1523
|
+
}).pipe(Effect8.ensuring(syncStatus.set("idle")), Effect8.retry({ schedule: Schedule.exponential(5000).pipe(Schedule.jittered), times: 10 }), Effect8.ignore);
|
|
1524
|
+
const scheduleAutoFlush = () => {
|
|
1525
|
+
if (autoFlushActive)
|
|
1526
|
+
return;
|
|
1527
|
+
autoFlushActive = true;
|
|
1528
|
+
forkHandled(autoFlushEffect.pipe(Effect8.ensuring(Effect8.sync(() => {
|
|
1529
|
+
autoFlushActive = false;
|
|
1530
|
+
}))));
|
|
1531
|
+
};
|
|
1532
|
+
const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
|
|
1533
|
+
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1534
|
+
if (!memberRecord)
|
|
1535
|
+
return false;
|
|
1536
|
+
return !!memberRecord.removedAt;
|
|
1537
|
+
});
|
|
1538
|
+
const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1539
|
+
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1540
|
+
if (existing)
|
|
1541
|
+
return null;
|
|
1542
|
+
const unwrapResult = yield* Effect8.result(giftWrapHandle.unwrap(remoteGw));
|
|
1543
|
+
if (unwrapResult._tag === "Failure") {
|
|
1544
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
const rumor = unwrapResult.success;
|
|
1548
|
+
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1549
|
+
if (!dTag) {
|
|
1550
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
const colonIdx = dTag.indexOf(":");
|
|
1554
|
+
if (colonIdx === -1) {
|
|
1555
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
const collectionName = dTag.substring(0, colonIdx);
|
|
1559
|
+
const recordId = dTag.substring(colonIdx + 1);
|
|
1560
|
+
const retention = knownCollections.get(collectionName);
|
|
1561
|
+
if (retention === undefined) {
|
|
1562
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
if (rumor.pubkey) {
|
|
1566
|
+
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1567
|
+
if (reject) {
|
|
1568
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
let data = null;
|
|
1573
|
+
let kind = "u";
|
|
1574
|
+
const parsed = yield* Effect8.try({
|
|
1575
|
+
try: () => JSON.parse(rumor.content),
|
|
1576
|
+
catch: () => {
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
}).pipe(Effect8.orElseSucceed(() => {
|
|
1580
|
+
return;
|
|
1581
|
+
}));
|
|
1582
|
+
if (parsed === undefined) {
|
|
1583
|
+
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
if (parsed === null || parsed._deleted) {
|
|
1587
|
+
kind = "d";
|
|
1588
|
+
} else {
|
|
1589
|
+
data = parsed;
|
|
1590
|
+
}
|
|
1591
|
+
const author = rumor.pubkey || undefined;
|
|
1592
|
+
const event = {
|
|
1593
|
+
id: rumor.id,
|
|
1594
|
+
collection: collectionName,
|
|
1595
|
+
recordId,
|
|
1596
|
+
kind,
|
|
1597
|
+
data,
|
|
1598
|
+
createdAt: rumor.created_at * 1000,
|
|
1599
|
+
author
|
|
1600
|
+
};
|
|
1601
|
+
yield* storage.putGiftWrap({
|
|
1602
|
+
id: remoteGw.id,
|
|
1603
|
+
createdAt: remoteGw.created_at
|
|
1604
|
+
});
|
|
1605
|
+
yield* storage.putEvent(event);
|
|
1606
|
+
const didApply = yield* applyEvent(storage, event);
|
|
1607
|
+
if (didApply && (kind === "u" || kind === "d")) {
|
|
1608
|
+
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1609
|
+
}
|
|
1610
|
+
if (author && onNewAuthor) {
|
|
1611
|
+
onNewAuthor(author);
|
|
1612
|
+
}
|
|
1613
|
+
return collectionName;
|
|
1614
|
+
});
|
|
1615
|
+
const processRealtimeGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1616
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
|
|
1617
|
+
if (collection2) {
|
|
1618
|
+
yield* notifyCollectionUpdated(collection2);
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
|
|
1622
|
+
const unwrapResult = yield* Effect8.result(Effect8.try({
|
|
1623
|
+
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1624
|
+
catch: (e) => new CryptoError({
|
|
1625
|
+
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
1626
|
+
cause: e
|
|
1627
|
+
})
|
|
1628
|
+
}));
|
|
1629
|
+
if (unwrapResult._tag === "Failure")
|
|
1630
|
+
return false;
|
|
1631
|
+
const rumor = unwrapResult.success;
|
|
1632
|
+
const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
|
|
1633
|
+
if (!dTag)
|
|
1634
|
+
return false;
|
|
1635
|
+
const removalNoticeOpt = parseRemovalNotice(rumor.content, dTag);
|
|
1636
|
+
if (Option5.isSome(removalNoticeOpt)) {
|
|
1637
|
+
if (onRemoved)
|
|
1638
|
+
onRemoved(removalNoticeOpt.value);
|
|
1639
|
+
return true;
|
|
1640
|
+
}
|
|
1641
|
+
const rotationDataOpt = parseRotationEvent(rumor.content, dTag);
|
|
1642
|
+
if (Option5.isNone(rotationDataOpt))
|
|
1643
|
+
return false;
|
|
1644
|
+
const rotationData = rotationDataOpt.value;
|
|
1645
|
+
if (epochStore.epochs.has(rotationData.epochId))
|
|
1646
|
+
return false;
|
|
1647
|
+
const epoch = createEpochKey(rotationData.epochId, rotationData.epochKey, rumor.pubkey || "", rotationData.parentEpoch);
|
|
1648
|
+
addEpoch(epochStore, epoch);
|
|
1649
|
+
epochStore.currentEpochId = epoch.id;
|
|
1650
|
+
let membersChanged = false;
|
|
1651
|
+
for (const removedPubkey of rotationData.removedMembers) {
|
|
1652
|
+
const memberRecord = yield* storage.getRecord("_members", removedPubkey);
|
|
1653
|
+
if (memberRecord && !memberRecord.removedAt) {
|
|
1654
|
+
yield* storage.putRecord("_members", {
|
|
1655
|
+
...memberRecord,
|
|
1656
|
+
removedAt: Date.now(),
|
|
1657
|
+
removedInEpoch: epoch.id
|
|
1658
|
+
});
|
|
1659
|
+
yield* notifyChange(watchCtx, {
|
|
1660
|
+
collection: "_members",
|
|
1661
|
+
recordId: removedPubkey,
|
|
1662
|
+
kind: "update"
|
|
1663
|
+
});
|
|
1664
|
+
membersChanged = true;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
if (membersChanged && onMembersChanged)
|
|
1668
|
+
onMembersChanged();
|
|
1669
|
+
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1670
|
+
return true;
|
|
1671
|
+
});
|
|
1672
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(relayUrls, (url) => Effect8.gen(function* () {
|
|
1673
|
+
yield* relay.subscribe(filter, url, (event) => {
|
|
1674
|
+
forkHandled(onEvent(event));
|
|
1675
|
+
}).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1676
|
+
}), { discard: true });
|
|
1677
|
+
const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1678
|
+
const reconcileResult = yield* Effect8.result(reconcileWithRelay(storage, relay, url, Array.from(pubKeys)));
|
|
1679
|
+
if (reconcileResult._tag === "Failure") {
|
|
1680
|
+
onSyncError?.(reconcileResult.failure);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
const { haveIds, needIds } = reconcileResult.success;
|
|
1684
|
+
if (needIds.length > 0) {
|
|
1685
|
+
const fetched = yield* relay.fetchEvents(needIds, url).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.orElseSucceed(() => []));
|
|
1686
|
+
const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
|
|
1687
|
+
yield* Effect8.forEach(sorted, (remoteGw) => Effect8.gen(function* () {
|
|
1688
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
|
|
1689
|
+
if (collection2)
|
|
1690
|
+
changedCollections.add(collection2);
|
|
1691
|
+
}), { discard: true });
|
|
1692
|
+
}
|
|
1693
|
+
if (haveIds.length > 0) {
|
|
1694
|
+
yield* Effect8.forEach(haveIds, (id) => Effect8.gen(function* () {
|
|
1695
|
+
const gw = yield* storage.getGiftWrap(id);
|
|
1696
|
+
if (!gw?.event)
|
|
1697
|
+
return;
|
|
1698
|
+
yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1699
|
+
}), { discard: true });
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
const handle = {
|
|
1703
|
+
sync: () => Effect8.gen(function* () {
|
|
1704
|
+
yield* syncStatus.set("syncing");
|
|
1705
|
+
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1706
|
+
const changedCollections = new Set;
|
|
1707
|
+
yield* Effect8.gen(function* () {
|
|
1708
|
+
const pubKeys = getSubscriptionPubKeys();
|
|
1709
|
+
yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
|
|
1710
|
+
discard: true
|
|
1711
|
+
});
|
|
1712
|
+
yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
|
|
1713
|
+
}).pipe(Effect8.ensuring(Effect8.gen(function* () {
|
|
1714
|
+
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1715
|
+
yield* syncStatus.set("idle");
|
|
1716
|
+
})));
|
|
1717
|
+
}),
|
|
1718
|
+
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1719
|
+
if (!giftWrap.event)
|
|
1720
|
+
return;
|
|
1721
|
+
yield* relay.publish(giftWrap.event, relayUrls).pipe(Effect8.tapError(() => storage.putGiftWrap(giftWrap).pipe(Effect8.andThen(publishQueue.enqueue(giftWrap.id)), Effect8.andThen(Effect8.sync(() => scheduleAutoFlush())))), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1722
|
+
}),
|
|
1723
|
+
startSubscription: () => Effect8.gen(function* () {
|
|
1724
|
+
const pubKeys = getSubscriptionPubKeys();
|
|
1725
|
+
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1726
|
+
if (!pubKeys.includes(personalPublicKey)) {
|
|
1727
|
+
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [personalPublicKey] }, (event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid));
|
|
1728
|
+
}
|
|
1729
|
+
}),
|
|
1730
|
+
addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap)
|
|
1731
|
+
};
|
|
1732
|
+
forkHandled(publishQueue.size().pipe(Effect8.flatMap((size) => Effect8.sync(() => {
|
|
1733
|
+
if (size > 0)
|
|
1734
|
+
scheduleAutoFlush();
|
|
1735
|
+
}))));
|
|
1736
|
+
return handle;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// src/db/members.ts
|
|
1740
|
+
import { Effect as Effect9, Option as Option6, Schema as Schema6 } from "effect";
|
|
1741
|
+
var optionalString = {
|
|
1742
|
+
_tag: "FieldDef",
|
|
1743
|
+
kind: "string",
|
|
1744
|
+
isOptional: true,
|
|
1745
|
+
isArray: false
|
|
1746
|
+
};
|
|
1747
|
+
var optionalNumber = {
|
|
1748
|
+
_tag: "FieldDef",
|
|
1749
|
+
kind: "number",
|
|
1750
|
+
isOptional: true,
|
|
1751
|
+
isArray: false
|
|
1752
|
+
};
|
|
1753
|
+
var requiredNumber = {
|
|
1754
|
+
_tag: "FieldDef",
|
|
1755
|
+
kind: "number",
|
|
1756
|
+
isOptional: false,
|
|
1757
|
+
isArray: false
|
|
1758
|
+
};
|
|
1759
|
+
var requiredString = {
|
|
1760
|
+
_tag: "FieldDef",
|
|
1761
|
+
kind: "string",
|
|
1762
|
+
isOptional: false,
|
|
1763
|
+
isArray: false
|
|
1764
|
+
};
|
|
1765
|
+
var membersCollectionDef = {
|
|
1766
|
+
_tag: "CollectionDef",
|
|
1767
|
+
name: "_members",
|
|
1768
|
+
fields: {
|
|
1769
|
+
name: optionalString,
|
|
1770
|
+
picture: optionalString,
|
|
1771
|
+
about: optionalString,
|
|
1772
|
+
nip05: optionalString,
|
|
1773
|
+
addedAt: requiredNumber,
|
|
1774
|
+
addedInEpoch: requiredString,
|
|
1775
|
+
removedAt: optionalNumber,
|
|
1776
|
+
removedInEpoch: optionalString
|
|
1777
|
+
},
|
|
1778
|
+
indices: [],
|
|
1779
|
+
eventRetention: 1
|
|
1780
|
+
};
|
|
1781
|
+
var AuthorProfileSchema = Schema6.Struct({
|
|
1782
|
+
name: Schema6.optionalKey(Schema6.String),
|
|
1783
|
+
picture: Schema6.optionalKey(Schema6.String),
|
|
1784
|
+
about: Schema6.optionalKey(Schema6.String),
|
|
1785
|
+
nip05: Schema6.optionalKey(Schema6.String)
|
|
1786
|
+
});
|
|
1787
|
+
var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(AuthorProfileSchema));
|
|
1788
|
+
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1789
|
+
return Effect9.gen(function* () {
|
|
1790
|
+
for (const url of relayUrls) {
|
|
1791
|
+
const result = yield* Effect9.result(relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url));
|
|
1792
|
+
if (result._tag === "Success" && result.success.length > 0) {
|
|
1793
|
+
return yield* decodeAuthorProfile(result.success[0].content).pipe(Effect9.map(Option6.some), Effect9.orElseSucceed(() => Option6.none()));
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return Option6.none();
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// src/services/Identity.ts
|
|
1801
|
+
import { ServiceMap as ServiceMap3 } from "effect";
|
|
1802
|
+
|
|
1803
|
+
class Identity extends ServiceMap3.Service()("tablinum/Identity") {
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/services/EpochStore.ts
|
|
1807
|
+
import { ServiceMap as ServiceMap4 } from "effect";
|
|
1808
|
+
|
|
1809
|
+
class EpochStore extends ServiceMap4.Service()("tablinum/EpochStore") {
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/services/Storage.ts
|
|
1813
|
+
import { ServiceMap as ServiceMap5 } from "effect";
|
|
1814
|
+
|
|
1815
|
+
class Storage extends ServiceMap5.Service()("tablinum/Storage") {
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// src/services/Relay.ts
|
|
1819
|
+
import { ServiceMap as ServiceMap6 } from "effect";
|
|
1820
|
+
|
|
1821
|
+
class Relay extends ServiceMap6.Service()("tablinum/Relay") {
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/services/GiftWrap.ts
|
|
1825
|
+
import { ServiceMap as ServiceMap7 } from "effect";
|
|
1826
|
+
|
|
1827
|
+
class GiftWrap3 extends ServiceMap7.Service()("tablinum/GiftWrap") {
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// src/services/PublishQueue.ts
|
|
1831
|
+
import { ServiceMap as ServiceMap8 } from "effect";
|
|
1832
|
+
|
|
1833
|
+
class PublishQueue extends ServiceMap8.Service()("tablinum/PublishQueue") {
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// src/services/SyncStatus.ts
|
|
1837
|
+
import { ServiceMap as ServiceMap9 } from "effect";
|
|
1838
|
+
|
|
1839
|
+
class SyncStatus extends ServiceMap9.Service()("tablinum/SyncStatus") {
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/layers/IdentityLive.ts
|
|
1843
|
+
import { Effect as Effect11, Layer } from "effect";
|
|
1844
|
+
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1845
|
+
|
|
1846
|
+
// src/db/identity.ts
|
|
1847
|
+
import { Effect as Effect10 } from "effect";
|
|
1848
|
+
import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
|
|
1849
|
+
import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils.js";
|
|
1850
|
+
function createIdentity(suppliedKey) {
|
|
1851
|
+
return Effect10.gen(function* () {
|
|
1852
|
+
let privateKey;
|
|
1853
|
+
if (suppliedKey) {
|
|
1854
|
+
if (suppliedKey.length !== 32) {
|
|
1855
|
+
return yield* new CryptoError({
|
|
1856
|
+
message: `Private key must be 32 bytes, got ${suppliedKey.length}`
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
privateKey = suppliedKey;
|
|
1860
|
+
} else {
|
|
1861
|
+
privateKey = new Uint8Array(32);
|
|
1862
|
+
crypto.getRandomValues(privateKey);
|
|
1863
|
+
}
|
|
1864
|
+
const privateKeyHex = bytesToHex2(privateKey);
|
|
1865
|
+
const publicKey = yield* Effect10.try({
|
|
1866
|
+
try: () => getPublicKey2(privateKey),
|
|
1867
|
+
catch: (e) => new CryptoError({
|
|
1868
|
+
message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
|
|
1869
|
+
cause: e
|
|
1870
|
+
})
|
|
1871
|
+
});
|
|
1872
|
+
return {
|
|
1873
|
+
privateKey,
|
|
1874
|
+
publicKey,
|
|
1875
|
+
exportKey: () => privateKeyHex
|
|
1876
|
+
};
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// src/layers/IdentityLive.ts
|
|
1881
|
+
var IdentityLive = Layer.effect(Identity, Effect11.gen(function* () {
|
|
1882
|
+
const config = yield* Config;
|
|
1883
|
+
const storage = yield* Storage;
|
|
1884
|
+
const idbKey = yield* storage.getMeta("identity_key");
|
|
1885
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : undefined);
|
|
1886
|
+
const identity = yield* createIdentity(resolvedKey);
|
|
1887
|
+
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
1888
|
+
return identity;
|
|
1889
|
+
}));
|
|
1890
|
+
|
|
1891
|
+
// src/layers/EpochStoreLive.ts
|
|
1892
|
+
import { Effect as Effect12, Layer as Layer2, Option as Option7 } from "effect";
|
|
1893
|
+
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1894
|
+
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
1895
|
+
var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
|
|
1896
|
+
const config = yield* Config;
|
|
1897
|
+
const identity = yield* Identity;
|
|
1898
|
+
const storage = yield* Storage;
|
|
1899
|
+
const idbRaw = yield* storage.getMeta("epochs");
|
|
1900
|
+
if (typeof idbRaw === "string") {
|
|
1901
|
+
const idbStore = deserializeEpochStore(idbRaw);
|
|
1902
|
+
if (Option7.isSome(idbStore)) {
|
|
1903
|
+
return idbStore.value;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
1907
|
+
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
1908
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
1909
|
+
return store2;
|
|
1910
|
+
}
|
|
1911
|
+
const store = createEpochStoreFromInputs([{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }], { createdBy: identity.publicKey });
|
|
1912
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
1913
|
+
return store;
|
|
1914
|
+
}));
|
|
1915
|
+
|
|
1916
|
+
// src/layers/StorageLive.ts
|
|
1917
|
+
import { Effect as Effect14, Layer as Layer3 } from "effect";
|
|
1918
|
+
|
|
1919
|
+
// src/storage/idb.ts
|
|
1920
|
+
import { Effect as Effect13 } from "effect";
|
|
1921
|
+
import { openDB } from "idb";
|
|
1922
|
+
var DB_NAME = "tablinum";
|
|
1923
|
+
function storeName(collection2) {
|
|
1924
|
+
return `col_${collection2}`;
|
|
1925
|
+
}
|
|
1926
|
+
function computeSchemaSig(schema) {
|
|
1927
|
+
return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
|
|
1928
|
+
const indices = [...def.indices ?? []].sort().join(",");
|
|
1929
|
+
return `${name}:${indices}`;
|
|
1930
|
+
}).join("|");
|
|
1931
|
+
}
|
|
1932
|
+
function wrap(label, fn) {
|
|
1933
|
+
return Effect13.tryPromise({
|
|
1934
|
+
try: fn,
|
|
1935
|
+
catch: (e) => new StorageError({
|
|
1936
|
+
message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
1937
|
+
cause: e
|
|
1938
|
+
})
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
function upgradeSchema(database, schema, tx) {
|
|
1942
|
+
if (!database.objectStoreNames.contains("_meta")) {
|
|
1943
|
+
database.createObjectStore("_meta");
|
|
1944
|
+
}
|
|
1945
|
+
if (!database.objectStoreNames.contains("events")) {
|
|
1946
|
+
const events = database.createObjectStore("events", { keyPath: "id" });
|
|
1947
|
+
events.createIndex("by-record", ["collection", "recordId"]);
|
|
1948
|
+
}
|
|
1949
|
+
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
1950
|
+
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
1951
|
+
}
|
|
1952
|
+
const expectedStores = new Set;
|
|
1953
|
+
for (const [, def] of Object.entries(schema)) {
|
|
1954
|
+
const sn = storeName(def.name);
|
|
1955
|
+
expectedStores.add(sn);
|
|
1956
|
+
if (!database.objectStoreNames.contains(sn)) {
|
|
1957
|
+
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
1958
|
+
for (const idx of def.indices ?? []) {
|
|
1959
|
+
store.createIndex(idx, idx);
|
|
1960
|
+
}
|
|
1961
|
+
} else {
|
|
1962
|
+
const store = tx.objectStore(sn);
|
|
1963
|
+
const existingIndices = new Set(Array.from(store.indexNames));
|
|
1964
|
+
const wantedIndices = new Set(def.indices ?? []);
|
|
1965
|
+
for (const idx of existingIndices) {
|
|
1966
|
+
if (!wantedIndices.has(idx))
|
|
1967
|
+
store.deleteIndex(idx);
|
|
1968
|
+
}
|
|
1969
|
+
for (const idx of wantedIndices) {
|
|
1970
|
+
if (!existingIndices.has(idx))
|
|
1971
|
+
store.createIndex(idx, idx);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
for (const existing of Array.from(database.objectStoreNames)) {
|
|
1976
|
+
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
1977
|
+
database.deleteObjectStore(existing);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
|
|
1981
|
+
}
|
|
1982
|
+
function openIDBStorage(dbName, schema) {
|
|
1983
|
+
return Effect13.gen(function* () {
|
|
1984
|
+
const name = dbName ?? DB_NAME;
|
|
1985
|
+
const schemaSig = computeSchemaSig(schema);
|
|
1986
|
+
const probeDb = yield* Effect13.tryPromise({
|
|
1987
|
+
try: () => openDB(name),
|
|
1988
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
1989
|
+
});
|
|
1990
|
+
const currentVersion = probeDb.version;
|
|
1991
|
+
let needsUpgrade = true;
|
|
1992
|
+
if (probeDb.objectStoreNames.contains("_meta")) {
|
|
1993
|
+
const storedSig = yield* Effect13.tryPromise({
|
|
1994
|
+
try: () => probeDb.get("_meta", "schema_sig"),
|
|
1995
|
+
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
1996
|
+
}).pipe(Effect13.catch(() => Effect13.succeed(undefined)));
|
|
1997
|
+
needsUpgrade = storedSig !== schemaSig;
|
|
1998
|
+
}
|
|
1999
|
+
probeDb.close();
|
|
2000
|
+
const db = needsUpgrade ? yield* Effect13.tryPromise({
|
|
2001
|
+
try: () => openDB(name, currentVersion + 1, {
|
|
2002
|
+
upgrade(database, _oldVersion, _newVersion, transaction) {
|
|
2003
|
+
upgradeSchema(database, schema, transaction);
|
|
2004
|
+
}
|
|
2005
|
+
}),
|
|
2006
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2007
|
+
}) : yield* Effect13.tryPromise({
|
|
2008
|
+
try: () => openDB(name),
|
|
2009
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2010
|
+
});
|
|
2011
|
+
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2012
|
+
const handle = {
|
|
2013
|
+
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => {
|
|
2014
|
+
return;
|
|
2015
|
+
})),
|
|
2016
|
+
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2017
|
+
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2018
|
+
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
2019
|
+
clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
|
|
2020
|
+
getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
|
|
2021
|
+
getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
|
|
2022
|
+
getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
|
|
2023
|
+
const sn = storeName(collection2);
|
|
2024
|
+
const tx = db.transaction(sn, "readonly");
|
|
2025
|
+
const store = tx.objectStore(sn);
|
|
2026
|
+
const index = store.index(indexName);
|
|
2027
|
+
const results = [];
|
|
2028
|
+
let cursor = await index.openCursor(null, direction ?? "next");
|
|
2029
|
+
while (cursor) {
|
|
2030
|
+
results.push(cursor.value);
|
|
2031
|
+
cursor = await cursor.continue();
|
|
2032
|
+
}
|
|
2033
|
+
return results;
|
|
2034
|
+
}),
|
|
2035
|
+
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => {
|
|
2036
|
+
return;
|
|
2037
|
+
})),
|
|
2038
|
+
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2039
|
+
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2040
|
+
getEventsByRecord: (collection2, recordId) => wrap("getEventsByRecord", () => db.getAllFromIndex("events", "by-record", [collection2, recordId])),
|
|
2041
|
+
putGiftWrap: (gw) => wrap("putGiftWrap", () => db.put("giftwraps", gw).then(() => {
|
|
2042
|
+
return;
|
|
2043
|
+
})),
|
|
2044
|
+
getGiftWrap: (id) => wrap("getGiftWrap", () => db.get("giftwraps", id)),
|
|
2045
|
+
getAllGiftWraps: () => wrap("getAllGiftWraps", () => db.getAll("giftwraps")),
|
|
2046
|
+
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => {
|
|
2047
|
+
return;
|
|
2048
|
+
})),
|
|
2049
|
+
stripGiftWrapBlob: (id) => wrap("stripGiftWrapBlob", async () => {
|
|
2050
|
+
const existing = await db.get("giftwraps", id);
|
|
2051
|
+
if (existing) {
|
|
2052
|
+
await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
|
|
2053
|
+
}
|
|
2054
|
+
}),
|
|
2055
|
+
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => {
|
|
2056
|
+
return;
|
|
2057
|
+
})),
|
|
2058
|
+
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2059
|
+
const existing = await db.get("events", id);
|
|
2060
|
+
if (existing) {
|
|
2061
|
+
await db.put("events", { ...existing, data: null });
|
|
2062
|
+
}
|
|
2063
|
+
}),
|
|
2064
|
+
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2065
|
+
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => {
|
|
2066
|
+
return;
|
|
2067
|
+
})),
|
|
2068
|
+
close: () => Effect13.sync(() => db.close())
|
|
2069
|
+
};
|
|
2070
|
+
return handle;
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/layers/StorageLive.ts
|
|
2075
|
+
var StorageLive = Layer3.effect(Storage, Effect14.gen(function* () {
|
|
2076
|
+
const config = yield* Config;
|
|
2077
|
+
return yield* openIDBStorage(config.dbName, {
|
|
2078
|
+
...config.schema,
|
|
2079
|
+
_members: membersCollectionDef
|
|
2080
|
+
});
|
|
2081
|
+
}));
|
|
2082
|
+
|
|
2083
|
+
// src/layers/RelayLive.ts
|
|
2084
|
+
import { Layer as Layer4 } from "effect";
|
|
2085
|
+
|
|
2086
|
+
// src/sync/relay.ts
|
|
2087
|
+
import { Effect as Effect15, Option as Option8, Schema as Schema7, ScopedCache, Scope as Scope3 } from "effect";
|
|
2088
|
+
import { Relay as Relay2 } from "nostr-tools/relay";
|
|
2089
|
+
var NegMessageFrameSchema = Schema7.Tuple([
|
|
2090
|
+
Schema7.Literal("NEG-MSG"),
|
|
2091
|
+
Schema7.String,
|
|
2092
|
+
Schema7.String
|
|
2093
|
+
]);
|
|
2094
|
+
var NegErrorFrameSchema = Schema7.Tuple([Schema7.Literal("NEG-ERR"), Schema7.String, Schema7.String]);
|
|
2095
|
+
var decodeNegFrame = Schema7.decodeUnknownEffect(Schema7.fromJsonString(Schema7.Union([NegMessageFrameSchema, NegErrorFrameSchema])));
|
|
2096
|
+
function parseNegMessageFrame(data) {
|
|
2097
|
+
return Effect15.runSync(decodeNegFrame(data).pipe(Effect15.map(Option8.some), Effect15.orElseSucceed(() => Option8.none())));
|
|
2098
|
+
}
|
|
2099
|
+
function createRelayHandle() {
|
|
2100
|
+
return Effect15.gen(function* () {
|
|
2101
|
+
const relayScope = yield* Effect15.scope;
|
|
2102
|
+
const connections = yield* ScopedCache.make({
|
|
2103
|
+
capacity: 64,
|
|
2104
|
+
lookup: (url) => Effect15.acquireRelease(Effect15.tryPromise({
|
|
2105
|
+
try: () => Relay2.connect(url),
|
|
2106
|
+
catch: (e) => new RelayError({
|
|
2107
|
+
message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2108
|
+
url,
|
|
2109
|
+
cause: e
|
|
2110
|
+
})
|
|
2111
|
+
}), (relay) => Effect15.sync(() => {
|
|
2112
|
+
relay.close();
|
|
2113
|
+
}))
|
|
2114
|
+
});
|
|
2115
|
+
const connectedUrls = new Set;
|
|
2116
|
+
const statusListeners = new Set;
|
|
2117
|
+
const notifyStatus = () => {
|
|
2118
|
+
const status = { connectedUrls: [...connectedUrls] };
|
|
2119
|
+
for (const listener of statusListeners)
|
|
2120
|
+
listener(status);
|
|
2121
|
+
};
|
|
2122
|
+
const markConnected = (url) => {
|
|
2123
|
+
if (!connectedUrls.has(url)) {
|
|
2124
|
+
connectedUrls.add(url);
|
|
2125
|
+
notifyStatus();
|
|
2126
|
+
}
|
|
2127
|
+
};
|
|
2128
|
+
const markDisconnected = (url) => {
|
|
2129
|
+
if (connectedUrls.has(url)) {
|
|
2130
|
+
connectedUrls.delete(url);
|
|
2131
|
+
notifyStatus();
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
const getRelay = (url) => ScopedCache.get(connections, url).pipe(Effect15.flatMap((relay) => relay.connected === false ? ScopedCache.invalidate(connections, url).pipe(Effect15.andThen(ScopedCache.get(connections, url))) : Effect15.succeed(relay)));
|
|
2135
|
+
const withRelay = (url, run) => getRelay(url).pipe(Effect15.tap(() => Effect15.sync(() => markConnected(url))), Effect15.flatMap((relay) => run(relay)), Effect15.tapError(() => ScopedCache.invalidate(connections, url).pipe(Effect15.tap(() => Effect15.sync(() => markDisconnected(url))))));
|
|
2136
|
+
const collectEvents = (url, filters) => withRelay(url, (relay) => Effect15.callback((resume) => {
|
|
2137
|
+
const events = [];
|
|
2138
|
+
let settled = false;
|
|
2139
|
+
let timer;
|
|
2140
|
+
let sub;
|
|
2141
|
+
const cleanup = () => {
|
|
2142
|
+
settled = true;
|
|
2143
|
+
if (timer !== undefined) {
|
|
2144
|
+
clearTimeout(timer);
|
|
2145
|
+
timer = undefined;
|
|
2146
|
+
}
|
|
2147
|
+
sub?.close();
|
|
2148
|
+
sub = undefined;
|
|
2149
|
+
};
|
|
2150
|
+
const fail = (e) => resume(Effect15.fail(new RelayError({
|
|
2151
|
+
message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2152
|
+
url,
|
|
2153
|
+
cause: e
|
|
2154
|
+
})));
|
|
2155
|
+
try {
|
|
2156
|
+
sub = relay.subscribe([...filters], {
|
|
2157
|
+
onevent(evt) {
|
|
2158
|
+
if (!settled) {
|
|
2159
|
+
events.push(evt);
|
|
2160
|
+
}
|
|
2161
|
+
},
|
|
2162
|
+
oneose() {
|
|
2163
|
+
if (settled)
|
|
2164
|
+
return;
|
|
2165
|
+
cleanup();
|
|
2166
|
+
resume(Effect15.succeed(events));
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
timer = setTimeout(() => {
|
|
2170
|
+
if (settled)
|
|
2171
|
+
return;
|
|
2172
|
+
cleanup();
|
|
2173
|
+
resume(Effect15.succeed(events));
|
|
2174
|
+
}, 1e4);
|
|
2175
|
+
} catch (e) {
|
|
2176
|
+
cleanup();
|
|
2177
|
+
fail(e);
|
|
2178
|
+
}
|
|
2179
|
+
return Effect15.sync(cleanup);
|
|
2180
|
+
}));
|
|
2181
|
+
return {
|
|
2182
|
+
publish: (event, urls) => Effect15.gen(function* () {
|
|
2183
|
+
const results = yield* Effect15.forEach(urls, (url) => Effect15.result(withRelay(url, (relay) => Effect15.tryPromise({
|
|
2184
|
+
try: () => relay.publish(event),
|
|
2185
|
+
catch: (e) => new RelayError({
|
|
2186
|
+
message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2187
|
+
url,
|
|
2188
|
+
cause: e
|
|
2189
|
+
})
|
|
2190
|
+
}).pipe(Effect15.timeoutOrElse({
|
|
2191
|
+
duration: "10 seconds",
|
|
2192
|
+
onTimeout: () => Effect15.fail(new RelayError({ message: `Publish to ${url} timed out`, url }))
|
|
2193
|
+
})))), { concurrency: "unbounded" });
|
|
2194
|
+
const failures = results.filter((r) => r._tag === "Failure");
|
|
2195
|
+
if (failures.length === urls.length && urls.length > 0) {
|
|
2196
|
+
return yield* new RelayError({
|
|
2197
|
+
message: `Publish failed on all ${urls.length} relays`
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
}),
|
|
2201
|
+
fetchEvents: (ids, url) => Effect15.gen(function* () {
|
|
2202
|
+
if (ids.length === 0)
|
|
2203
|
+
return [];
|
|
2204
|
+
return yield* collectEvents(url, [{ ids }]);
|
|
2205
|
+
}),
|
|
2206
|
+
fetchByFilter: (filter, url) => collectEvents(url, [filter]),
|
|
2207
|
+
subscribe: (filter, url, onEvent) => withRelay(url, (relay) => Effect15.acquireRelease(Effect15.try({
|
|
2208
|
+
try: () => relay.subscribe([filter], {
|
|
2209
|
+
onevent(evt) {
|
|
2210
|
+
onEvent(evt);
|
|
2211
|
+
},
|
|
2212
|
+
oneose() {}
|
|
2213
|
+
}),
|
|
2214
|
+
catch: (e) => new RelayError({
|
|
2215
|
+
message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2216
|
+
url,
|
|
2217
|
+
cause: e
|
|
2218
|
+
})
|
|
2219
|
+
}), (sub) => Effect15.sync(() => {
|
|
2220
|
+
sub.close();
|
|
2221
|
+
})).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)),
|
|
2222
|
+
sendNegMsg: (url, subId, filter, msgHex) => withRelay(url, (relay) => Effect15.callback((resume) => {
|
|
2223
|
+
let settled = false;
|
|
2224
|
+
let timer;
|
|
2225
|
+
let sub;
|
|
2226
|
+
let ws;
|
|
2227
|
+
const cleanup = () => {
|
|
2228
|
+
settled = true;
|
|
2229
|
+
if (timer !== undefined) {
|
|
2230
|
+
clearTimeout(timer);
|
|
2231
|
+
timer = undefined;
|
|
2232
|
+
}
|
|
2233
|
+
sub?.close();
|
|
2234
|
+
sub = undefined;
|
|
2235
|
+
ws?.removeEventListener("message", handler);
|
|
2236
|
+
ws = undefined;
|
|
2237
|
+
};
|
|
2238
|
+
const fail = (e) => resume(Effect15.fail(new RelayError({
|
|
2239
|
+
message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2240
|
+
url,
|
|
2241
|
+
cause: e
|
|
2242
|
+
})));
|
|
2243
|
+
const handler = (msg) => {
|
|
2244
|
+
if (settled || typeof msg.data !== "string")
|
|
2245
|
+
return;
|
|
2246
|
+
const frameOpt = parseNegMessageFrame(msg.data);
|
|
2247
|
+
if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId)
|
|
2248
|
+
return;
|
|
2249
|
+
const frame = frameOpt.value;
|
|
2250
|
+
cleanup();
|
|
2251
|
+
if (frame[0] === "NEG-MSG") {
|
|
2252
|
+
resume(Effect15.succeed({
|
|
2253
|
+
msgHex: frame[2],
|
|
2254
|
+
haveIds: [],
|
|
2255
|
+
needIds: []
|
|
2256
|
+
}));
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
fail(new Error(`NEG-ERR: ${frame[2]}`));
|
|
2260
|
+
};
|
|
2261
|
+
try {
|
|
2262
|
+
sub = relay.subscribe([filter], {
|
|
2263
|
+
onevent() {},
|
|
2264
|
+
oneose() {}
|
|
2265
|
+
});
|
|
2266
|
+
ws = relay._ws || relay.ws;
|
|
2267
|
+
if (!ws) {
|
|
2268
|
+
cleanup();
|
|
2269
|
+
fail(new Error("Cannot access relay WebSocket"));
|
|
2270
|
+
return Effect15.succeed(undefined);
|
|
2271
|
+
}
|
|
2272
|
+
timer = setTimeout(() => {
|
|
2273
|
+
if (settled)
|
|
2274
|
+
return;
|
|
2275
|
+
cleanup();
|
|
2276
|
+
fail(new Error("NIP-77 negotiation timeout"));
|
|
2277
|
+
}, 30000);
|
|
2278
|
+
ws.addEventListener("message", handler);
|
|
2279
|
+
ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
|
|
2280
|
+
} catch (e) {
|
|
2281
|
+
cleanup();
|
|
2282
|
+
fail(e);
|
|
2283
|
+
}
|
|
2284
|
+
return Effect15.sync(cleanup);
|
|
2285
|
+
})),
|
|
2286
|
+
closeAll: () => ScopedCache.invalidateAll(connections),
|
|
2287
|
+
getStatus: () => ({ connectedUrls: [...connectedUrls] }),
|
|
2288
|
+
subscribeStatus: (callback) => {
|
|
2289
|
+
statusListeners.add(callback);
|
|
2290
|
+
return () => statusListeners.delete(callback);
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// src/layers/RelayLive.ts
|
|
2297
|
+
var RelayLive = Layer4.effect(Relay, createRelayHandle());
|
|
2298
|
+
|
|
2299
|
+
// src/layers/GiftWrapLive.ts
|
|
2300
|
+
import { Effect as Effect17, Layer as Layer5 } from "effect";
|
|
2301
|
+
|
|
2302
|
+
// src/sync/gift-wrap.ts
|
|
2303
|
+
import { Effect as Effect16 } from "effect";
|
|
2304
|
+
import { wrapEvent as wrapEvent2, unwrapEvent as unwrapEvent2 } from "nostr-tools/nip59";
|
|
2305
|
+
function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
2306
|
+
return {
|
|
2307
|
+
wrap: (rumor) => Effect16.try({
|
|
2308
|
+
try: () => wrapEvent2(rumor, senderPrivateKey, getCurrentPublicKey(epochStore)),
|
|
2309
|
+
catch: (e) => new CryptoError({
|
|
2310
|
+
message: `Gift wrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2311
|
+
cause: e
|
|
2312
|
+
})
|
|
2313
|
+
}),
|
|
2314
|
+
unwrap: (giftWrap) => Effect16.gen(function* () {
|
|
2315
|
+
const pTag = giftWrap.tags.find((t) => t[0] === "p")?.[1];
|
|
2316
|
+
if (!pTag) {
|
|
2317
|
+
return yield* new CryptoError({ message: "Gift wrap missing #p tag" });
|
|
2318
|
+
}
|
|
2319
|
+
const decKey = getDecryptionKey(epochStore, pTag);
|
|
2320
|
+
if (!decKey) {
|
|
2321
|
+
return yield* new CryptoError({
|
|
2322
|
+
message: `No epoch key for public key ${pTag.slice(0, 8)}...`
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
return yield* Effect16.try({
|
|
2326
|
+
try: () => unwrapEvent2(giftWrap, decKey),
|
|
2327
|
+
catch: (e) => new CryptoError({
|
|
2328
|
+
message: `Gift unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2329
|
+
cause: e
|
|
2330
|
+
})
|
|
2331
|
+
});
|
|
2332
|
+
})
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// src/layers/GiftWrapLive.ts
|
|
2337
|
+
var GiftWrapLive = Layer5.effect(GiftWrap3, Effect17.gen(function* () {
|
|
2338
|
+
const identity = yield* Identity;
|
|
2339
|
+
const epochStore = yield* EpochStore;
|
|
2340
|
+
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2341
|
+
}));
|
|
2342
|
+
|
|
2343
|
+
// src/layers/PublishQueueLive.ts
|
|
2344
|
+
import { Effect as Effect19, Layer as Layer6 } from "effect";
|
|
2345
|
+
|
|
2346
|
+
// src/sync/publish-queue.ts
|
|
2347
|
+
import { Effect as Effect18, Ref as Ref4 } from "effect";
|
|
2348
|
+
var META_KEY = "publish_queue";
|
|
2349
|
+
function persist(storage, pending) {
|
|
2350
|
+
return storage.putMeta(META_KEY, [...pending]);
|
|
2351
|
+
}
|
|
2352
|
+
function createPublishQueue(storage, relay) {
|
|
2353
|
+
return Effect18.gen(function* () {
|
|
2354
|
+
const stored = yield* storage.getMeta(META_KEY);
|
|
2355
|
+
const initial = Array.isArray(stored) ? new Set(stored) : new Set;
|
|
2356
|
+
const pendingRef = yield* Ref4.make(initial);
|
|
2357
|
+
const listeners = new Set;
|
|
2358
|
+
const notify = (pending) => {
|
|
2359
|
+
for (const listener of listeners)
|
|
2360
|
+
listener(pending.size);
|
|
2361
|
+
};
|
|
2362
|
+
return {
|
|
2363
|
+
enqueue: (eventId) => Effect18.gen(function* () {
|
|
2364
|
+
const next = yield* Ref4.updateAndGet(pendingRef, (set) => {
|
|
2365
|
+
const n = new Set(set);
|
|
2366
|
+
n.add(eventId);
|
|
2367
|
+
return n;
|
|
2368
|
+
});
|
|
2369
|
+
yield* persist(storage, next);
|
|
2370
|
+
notify(next);
|
|
2371
|
+
}),
|
|
2372
|
+
flush: (relayUrls) => Effect18.gen(function* () {
|
|
2373
|
+
const pending = yield* Ref4.get(pendingRef);
|
|
2374
|
+
if (pending.size === 0)
|
|
2375
|
+
return;
|
|
2376
|
+
const succeeded = new Set;
|
|
2377
|
+
let consecutiveFailures = 0;
|
|
2378
|
+
for (const eventId of pending) {
|
|
2379
|
+
if (consecutiveFailures >= 3)
|
|
2380
|
+
break;
|
|
2381
|
+
const gw = yield* storage.getGiftWrap(eventId);
|
|
2382
|
+
if (!gw || !gw.event) {
|
|
2383
|
+
succeeded.add(eventId);
|
|
2384
|
+
consecutiveFailures = 0;
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
const result = yield* Effect18.result(relay.publish(gw.event, relayUrls));
|
|
2388
|
+
if (result._tag === "Success") {
|
|
2389
|
+
succeeded.add(eventId);
|
|
2390
|
+
yield* storage.stripGiftWrapBlob(eventId);
|
|
2391
|
+
consecutiveFailures = 0;
|
|
2392
|
+
} else {
|
|
2393
|
+
consecutiveFailures++;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
if (succeeded.size > 0) {
|
|
2397
|
+
const updated = yield* Ref4.updateAndGet(pendingRef, (set) => {
|
|
2398
|
+
const next = new Set(set);
|
|
2399
|
+
for (const id of succeeded) {
|
|
2400
|
+
next.delete(id);
|
|
2401
|
+
}
|
|
2402
|
+
return next;
|
|
2403
|
+
});
|
|
2404
|
+
yield* persist(storage, updated);
|
|
2405
|
+
notify(updated);
|
|
2406
|
+
}
|
|
2407
|
+
}),
|
|
2408
|
+
size: () => Ref4.get(pendingRef).pipe(Effect18.map((s) => s.size)),
|
|
2409
|
+
subscribe: (callback) => {
|
|
2410
|
+
listeners.add(callback);
|
|
2411
|
+
return () => listeners.delete(callback);
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
// src/layers/PublishQueueLive.ts
|
|
2418
|
+
var PublishQueueLive = Layer6.effect(PublishQueue, Effect19.gen(function* () {
|
|
2419
|
+
const storage = yield* Storage;
|
|
2420
|
+
const relay = yield* Relay;
|
|
2421
|
+
return yield* createPublishQueue(storage, relay);
|
|
2422
|
+
}));
|
|
2423
|
+
|
|
2424
|
+
// src/layers/SyncStatusLive.ts
|
|
2425
|
+
import { Layer as Layer7 } from "effect";
|
|
2426
|
+
|
|
2427
|
+
// src/sync/sync-status.ts
|
|
2428
|
+
import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
2429
|
+
function createSyncStatusHandle() {
|
|
2430
|
+
return Effect20.gen(function* () {
|
|
2431
|
+
const ref = yield* SubscriptionRef.make("idle");
|
|
2432
|
+
const listeners = new Set;
|
|
2433
|
+
return {
|
|
2434
|
+
get: () => SubscriptionRef.get(ref),
|
|
2435
|
+
set: (status) => Effect20.gen(function* () {
|
|
2436
|
+
yield* SubscriptionRef.set(ref, status);
|
|
2437
|
+
for (const listener of listeners)
|
|
2438
|
+
listener(status);
|
|
2439
|
+
}),
|
|
2440
|
+
subscribe: (callback) => {
|
|
2441
|
+
listeners.add(callback);
|
|
2442
|
+
return () => listeners.delete(callback);
|
|
1572
2443
|
}
|
|
1573
|
-
}
|
|
1574
|
-
|
|
2444
|
+
};
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// src/layers/SyncStatusLive.ts
|
|
2449
|
+
var SyncStatusLive = Layer7.effect(SyncStatus, createSyncStatusHandle());
|
|
2450
|
+
|
|
2451
|
+
// src/layers/TablinumLive.ts
|
|
2452
|
+
function reportSyncError(onSyncError, error) {
|
|
2453
|
+
if (!onSyncError)
|
|
2454
|
+
return;
|
|
2455
|
+
onSyncError(error instanceof Error ? error : new Error(String(error)));
|
|
2456
|
+
}
|
|
2457
|
+
function mapMemberRecord(record) {
|
|
2458
|
+
return {
|
|
2459
|
+
id: record.id,
|
|
2460
|
+
addedAt: record.addedAt,
|
|
2461
|
+
addedInEpoch: record.addedInEpoch,
|
|
2462
|
+
...record.name !== undefined ? { name: record.name } : {},
|
|
2463
|
+
...record.picture !== undefined ? { picture: record.picture } : {},
|
|
2464
|
+
...record.about !== undefined ? { about: record.about } : {},
|
|
2465
|
+
...record.nip05 !== undefined ? { nip05: record.nip05 } : {},
|
|
2466
|
+
...record.removedAt !== undefined ? { removedAt: record.removedAt } : {},
|
|
2467
|
+
...record.removedInEpoch !== undefined ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
var IdentityWithDeps = IdentityLive.pipe(Layer8.provide(StorageLive));
|
|
2471
|
+
var EpochStoreWithDeps = EpochStoreLive.pipe(Layer8.provide(IdentityWithDeps), Layer8.provide(StorageLive));
|
|
2472
|
+
var GiftWrapWithDeps = GiftWrapLive.pipe(Layer8.provide(IdentityWithDeps), Layer8.provide(EpochStoreWithDeps));
|
|
2473
|
+
var PublishQueueWithDeps = PublishQueueLive.pipe(Layer8.provide(StorageLive), Layer8.provide(RelayLive));
|
|
2474
|
+
var AllServicesLive = Layer8.mergeAll(IdentityWithDeps, EpochStoreWithDeps, StorageLive, RelayLive, GiftWrapWithDeps, PublishQueueWithDeps, SyncStatusLive);
|
|
2475
|
+
var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
2476
|
+
const config = yield* Config;
|
|
2477
|
+
const identity = yield* Identity;
|
|
2478
|
+
const epochStore = yield* EpochStore;
|
|
2479
|
+
const storage = yield* Storage;
|
|
2480
|
+
const relay = yield* Relay;
|
|
2481
|
+
const giftWrap = yield* GiftWrap3;
|
|
2482
|
+
const publishQueue = yield* PublishQueue;
|
|
2483
|
+
const syncStatus = yield* SyncStatus;
|
|
2484
|
+
const scope = yield* Effect21.scope;
|
|
2485
|
+
const pubsub = yield* PubSub2.unbounded();
|
|
2486
|
+
const replayingRef = yield* Ref5.make(false);
|
|
2487
|
+
const closedRef = yield* Ref5.make(false);
|
|
2488
|
+
const watchCtx = { pubsub, replayingRef };
|
|
2489
|
+
const schemaEntries = Object.entries(config.schema);
|
|
2490
|
+
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2491
|
+
const knownCollections = new Map(allSchemaEntries.map(([, def]) => [def.name, def.eventRetention]));
|
|
2492
|
+
let notifyAuthor;
|
|
2493
|
+
const syncHandle = createSyncHandle(storage, giftWrap, relay, publishQueue, syncStatus, watchCtx, config.relays, knownCollections, epochStore, identity.privateKey, identity.publicKey, scope, config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : undefined, (pubkey) => notifyAuthor?.(pubkey), config.onRemoved, config.onMembersChanged);
|
|
2494
|
+
const onWrite = (event) => Effect21.gen(function* () {
|
|
2495
|
+
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2496
|
+
const dTag = `${event.collection}:${event.recordId}`;
|
|
2497
|
+
const wrapResult = yield* Effect21.result(giftWrap.wrap({
|
|
2498
|
+
kind: 1,
|
|
2499
|
+
content,
|
|
2500
|
+
tags: [["d", dTag]],
|
|
2501
|
+
created_at: Math.floor(event.createdAt / 1000)
|
|
2502
|
+
}));
|
|
2503
|
+
if (wrapResult._tag === "Failure") {
|
|
2504
|
+
reportSyncError(config.onSyncError, wrapResult.failure);
|
|
2505
|
+
return;
|
|
1575
2506
|
}
|
|
2507
|
+
const gw = wrapResult.success;
|
|
2508
|
+
yield* storage.putGiftWrap({ id: gw.id, createdAt: gw.created_at });
|
|
2509
|
+
yield* Effect21.forkIn(Effect21.gen(function* () {
|
|
2510
|
+
const publishResult = yield* Effect21.result(syncHandle.publishLocal({
|
|
2511
|
+
id: gw.id,
|
|
2512
|
+
event: gw,
|
|
2513
|
+
createdAt: gw.created_at
|
|
2514
|
+
}));
|
|
2515
|
+
if (publishResult._tag === "Failure") {
|
|
2516
|
+
reportSyncError(config.onSyncError, publishResult.failure);
|
|
2517
|
+
}
|
|
2518
|
+
}), scope);
|
|
2519
|
+
});
|
|
2520
|
+
const knownAuthors = new Set;
|
|
2521
|
+
const putMemberRecord = (record) => Effect21.gen(function* () {
|
|
2522
|
+
const existing = yield* storage.getRecord("_members", record.id);
|
|
1576
2523
|
const event = {
|
|
1577
|
-
id:
|
|
1578
|
-
collection:
|
|
1579
|
-
recordId,
|
|
1580
|
-
kind,
|
|
1581
|
-
data,
|
|
1582
|
-
createdAt:
|
|
2524
|
+
id: uuidv7(),
|
|
2525
|
+
collection: "_members",
|
|
2526
|
+
recordId: record.id,
|
|
2527
|
+
kind: existing ? "u" : "c",
|
|
2528
|
+
data: record,
|
|
2529
|
+
createdAt: Date.now(),
|
|
2530
|
+
author: identity.publicKey
|
|
1583
2531
|
};
|
|
1584
2532
|
yield* storage.putEvent(event);
|
|
1585
2533
|
yield* applyEvent(storage, event);
|
|
1586
|
-
|
|
2534
|
+
yield* onWrite(event);
|
|
2535
|
+
yield* notifyChange(watchCtx, {
|
|
2536
|
+
collection: "_members",
|
|
2537
|
+
recordId: record.id,
|
|
2538
|
+
kind: existing ? "update" : "create"
|
|
2539
|
+
});
|
|
2540
|
+
config.onMembersChanged?.();
|
|
1587
2541
|
});
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
2542
|
+
notifyAuthor = (pubkey) => {
|
|
2543
|
+
if (knownAuthors.has(pubkey))
|
|
2544
|
+
return;
|
|
2545
|
+
knownAuthors.add(pubkey);
|
|
2546
|
+
Effect21.runFork(Effect21.gen(function* () {
|
|
2547
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
2548
|
+
if (!existing) {
|
|
2549
|
+
yield* putMemberRecord({
|
|
2550
|
+
id: pubkey,
|
|
2551
|
+
addedAt: Date.now(),
|
|
2552
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none())));
|
|
2556
|
+
if (Option9.isSome(profileOpt)) {
|
|
2557
|
+
const current = yield* storage.getRecord("_members", pubkey);
|
|
2558
|
+
if (current) {
|
|
2559
|
+
yield* storage.putRecord("_members", {
|
|
2560
|
+
...current,
|
|
2561
|
+
...profileOpt.value
|
|
2562
|
+
});
|
|
2563
|
+
yield* notifyChange(watchCtx, {
|
|
2564
|
+
collection: "_members",
|
|
2565
|
+
recordId: pubkey,
|
|
2566
|
+
kind: "update"
|
|
2567
|
+
});
|
|
2568
|
+
config.onMembersChanged?.();
|
|
1618
2569
|
}
|
|
1619
|
-
yield* Effect13.result(publishQueue.flush(relayUrls));
|
|
1620
|
-
} finally {
|
|
1621
|
-
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1622
|
-
yield* syncStatus.set("idle");
|
|
1623
2570
|
}
|
|
2571
|
+
}).pipe(Effect21.ignore, Effect21.forkIn(scope)));
|
|
2572
|
+
};
|
|
2573
|
+
const handles = new Map;
|
|
2574
|
+
for (const [, def] of allSchemaEntries) {
|
|
2575
|
+
const validator = buildValidator(def.name, def);
|
|
2576
|
+
const partialValidator = buildPartialValidator(def.name, def);
|
|
2577
|
+
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite);
|
|
2578
|
+
handles.set(def.name, handle);
|
|
2579
|
+
}
|
|
2580
|
+
yield* syncHandle.startSubscription();
|
|
2581
|
+
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2582
|
+
if (!selfMember) {
|
|
2583
|
+
yield* putMemberRecord({
|
|
2584
|
+
id: identity.publicKey,
|
|
2585
|
+
addedAt: Date.now(),
|
|
2586
|
+
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
const ensureOpen = (effect) => Effect21.gen(function* () {
|
|
2590
|
+
if (yield* Ref5.get(closedRef)) {
|
|
2591
|
+
return yield* new StorageError({ message: "Database is closed" });
|
|
2592
|
+
}
|
|
2593
|
+
return yield* effect;
|
|
2594
|
+
});
|
|
2595
|
+
const ensureSyncOpen = (effect) => Effect21.gen(function* () {
|
|
2596
|
+
if (yield* Ref5.get(closedRef)) {
|
|
2597
|
+
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
2598
|
+
}
|
|
2599
|
+
return yield* effect;
|
|
2600
|
+
});
|
|
2601
|
+
const dbHandle = {
|
|
2602
|
+
collection: (name) => {
|
|
2603
|
+
const handle = handles.get(name);
|
|
2604
|
+
if (!handle)
|
|
2605
|
+
throw new Error(`Collection "${name}" not found in schema`);
|
|
2606
|
+
return handle;
|
|
2607
|
+
},
|
|
2608
|
+
publicKey: identity.publicKey,
|
|
2609
|
+
members: handles.get("_members"),
|
|
2610
|
+
exportKey: () => identity.exportKey(),
|
|
2611
|
+
exportInvite: () => ({
|
|
2612
|
+
epochKeys: [...exportEpochKeys(epochStore)],
|
|
2613
|
+
relays: [...config.relays],
|
|
2614
|
+
dbName: config.dbName
|
|
1624
2615
|
}),
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
if (onSyncError)
|
|
1631
|
-
onSyncError(result.failure);
|
|
1632
|
-
}
|
|
2616
|
+
close: () => Effect21.gen(function* () {
|
|
2617
|
+
if (yield* Ref5.get(closedRef))
|
|
2618
|
+
return;
|
|
2619
|
+
yield* Ref5.set(closedRef, true);
|
|
2620
|
+
yield* Scope4.close(scope, Exit.void);
|
|
1633
2621
|
}),
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
2622
|
+
rebuild: () => ensureOpen(rebuild(storage, allSchemaEntries.map(([, def]) => def.name))),
|
|
2623
|
+
sync: () => ensureSyncOpen(syncHandle.sync()),
|
|
2624
|
+
getSyncStatus: () => syncStatus.get(),
|
|
2625
|
+
subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
|
|
2626
|
+
pendingCount: () => publishQueue.size(),
|
|
2627
|
+
subscribePendingCount: (callback) => publishQueue.subscribe(callback),
|
|
2628
|
+
getRelayStatus: () => relay.getStatus(),
|
|
2629
|
+
subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
|
|
2630
|
+
addMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
|
|
2631
|
+
const existing = yield* storage.getRecord("_members", pubkey);
|
|
2632
|
+
if (existing && !existing.removedAt)
|
|
2633
|
+
return;
|
|
2634
|
+
yield* putMemberRecord({
|
|
2635
|
+
id: pubkey,
|
|
2636
|
+
addedAt: Date.now(),
|
|
2637
|
+
addedInEpoch: getCurrentEpoch(epochStore).id,
|
|
2638
|
+
...existing ? { removedAt: undefined, removedInEpoch: undefined } : {}
|
|
2639
|
+
});
|
|
2640
|
+
})),
|
|
2641
|
+
removeMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
|
|
2642
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
2643
|
+
const activeMembers = allMembers.filter((member) => !member.removedAt && member.id !== pubkey);
|
|
2644
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
2645
|
+
const result = createRotation(epochStore, identity.privateKey, identity.publicKey, activePubkeys, [pubkey]);
|
|
2646
|
+
addEpoch(epochStore, result.epoch);
|
|
2647
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
2648
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
2649
|
+
const memberRecord = yield* storage.getRecord("_members", pubkey);
|
|
2650
|
+
yield* putMemberRecord({
|
|
2651
|
+
...memberRecord ?? {
|
|
2652
|
+
id: pubkey,
|
|
2653
|
+
addedAt: 0,
|
|
2654
|
+
addedInEpoch: EpochId("epoch-0")
|
|
2655
|
+
},
|
|
2656
|
+
removedAt: Date.now(),
|
|
2657
|
+
removedInEpoch: result.epoch.id
|
|
2658
|
+
});
|
|
2659
|
+
yield* Effect21.forEach(result.wrappedEvents, (wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))), Effect21.ignore), { discard: true });
|
|
2660
|
+
yield* Effect21.forEach(result.removalNotices, (notice) => relay.publish(notice, [...config.relays]).pipe(Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))), Effect21.ignore), { discard: true });
|
|
2661
|
+
yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
|
|
2662
|
+
})),
|
|
2663
|
+
getMembers: () => ensureOpen(Effect21.gen(function* () {
|
|
2664
|
+
const allRecords = yield* storage.getAllRecords("_members");
|
|
2665
|
+
return allRecords.filter((record) => !record._d).map(mapMemberRecord);
|
|
2666
|
+
})),
|
|
2667
|
+
getProfile: () => ensureOpen(Effect21.gen(function* () {
|
|
2668
|
+
const record = yield* storage.getRecord("_members", identity.publicKey);
|
|
2669
|
+
if (!record)
|
|
2670
|
+
return {};
|
|
2671
|
+
const profile = {};
|
|
2672
|
+
if (record.name !== undefined)
|
|
2673
|
+
profile.name = record.name;
|
|
2674
|
+
if (record.picture !== undefined)
|
|
2675
|
+
profile.picture = record.picture;
|
|
2676
|
+
if (record.about !== undefined)
|
|
2677
|
+
profile.about = record.about;
|
|
2678
|
+
if (record.nip05 !== undefined)
|
|
2679
|
+
profile.nip05 = record.nip05;
|
|
2680
|
+
return profile;
|
|
2681
|
+
})),
|
|
2682
|
+
setProfile: (profile) => ensureOpen(Effect21.gen(function* () {
|
|
2683
|
+
const existing = yield* storage.getRecord("_members", identity.publicKey);
|
|
2684
|
+
if (!existing) {
|
|
2685
|
+
return yield* new ValidationError({ message: "Current user is not a member" });
|
|
1655
2686
|
}
|
|
1656
|
-
|
|
2687
|
+
const { _d, _u, _a, _e, ...memberFields } = existing;
|
|
2688
|
+
yield* putMemberRecord({ ...memberFields, ...profile });
|
|
2689
|
+
}))
|
|
1657
2690
|
};
|
|
1658
|
-
|
|
2691
|
+
return dbHandle;
|
|
2692
|
+
})).pipe(Layer8.provide(AllServicesLive));
|
|
1659
2693
|
|
|
1660
2694
|
// src/db/create-tablinum.ts
|
|
1661
|
-
function
|
|
1662
|
-
return
|
|
1663
|
-
if (
|
|
1664
|
-
return yield* new ValidationError({
|
|
1665
|
-
message: "At least one relay URL is required"
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
const schemaEntries = Object.entries(config.schema);
|
|
1669
|
-
if (schemaEntries.length === 0) {
|
|
2695
|
+
function validateConfig(config) {
|
|
2696
|
+
return Effect22.gen(function* () {
|
|
2697
|
+
if (Object.keys(config.schema).length === 0) {
|
|
1670
2698
|
return yield* new ValidationError({
|
|
1671
2699
|
message: "Schema must contain at least one collection"
|
|
1672
2700
|
});
|
|
1673
2701
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
const identity = yield* createIdentity(resolvedKey);
|
|
1687
|
-
if (typeof globalThis.localStorage !== "undefined") {
|
|
1688
|
-
globalThis.localStorage.setItem(storageKeyName, identity.exportKey());
|
|
1689
|
-
}
|
|
1690
|
-
const storage = yield* openIDBStorage(config.dbName, config.schema);
|
|
1691
|
-
const pubsub = yield* PubSub2.unbounded();
|
|
1692
|
-
const replayingRef = yield* Ref5.make(false);
|
|
1693
|
-
const watchCtx = { pubsub, replayingRef };
|
|
1694
|
-
const closedRef = yield* Ref5.make(false);
|
|
1695
|
-
const giftWrapHandle = createGiftWrapHandle(identity.privateKey, identity.publicKey);
|
|
1696
|
-
const relayHandle = createRelayHandle();
|
|
1697
|
-
const publishQueue = yield* createPublishQueue(storage, relayHandle);
|
|
1698
|
-
const syncStatus = yield* createSyncStatusHandle();
|
|
1699
|
-
const syncHandle = createSyncHandle(storage, giftWrapHandle, relayHandle, publishQueue, syncStatus, watchCtx, config.relays, identity.publicKey, config.onSyncError);
|
|
1700
|
-
const onWrite = (event) => Effect14.gen(function* () {
|
|
1701
|
-
console.log("[tablinum:onWrite]", event.kind, event.collection, event.recordId);
|
|
1702
|
-
const content = event.kind === "delete" ? JSON.stringify({ _deleted: true }) : JSON.stringify(event.data);
|
|
1703
|
-
const dTag = `${event.collection}:${event.recordId}`;
|
|
1704
|
-
const wrapResult = yield* Effect14.result(giftWrapHandle.wrap({
|
|
1705
|
-
kind: 1,
|
|
1706
|
-
content,
|
|
1707
|
-
tags: [["d", dTag]],
|
|
1708
|
-
created_at: Math.floor(event.createdAt / 1000)
|
|
1709
|
-
}));
|
|
1710
|
-
if (wrapResult._tag === "Success") {
|
|
1711
|
-
const gw = wrapResult.success;
|
|
1712
|
-
console.log("[tablinum:onWrite] gift wrap created:", gw.id, "kind:", gw.kind, "tags:", JSON.stringify(gw.tags));
|
|
1713
|
-
yield* storage.putGiftWrap({
|
|
1714
|
-
id: gw.id,
|
|
1715
|
-
event: gw,
|
|
1716
|
-
createdAt: gw.created_at
|
|
1717
|
-
});
|
|
1718
|
-
console.log("[tablinum:onWrite] gift wrap stored, publishing...");
|
|
1719
|
-
const publishEffect = Effect14.gen(function* () {
|
|
1720
|
-
const pubResult = yield* Effect14.result(syncHandle.publishLocal({
|
|
1721
|
-
id: gw.id,
|
|
1722
|
-
event: gw,
|
|
1723
|
-
createdAt: gw.created_at
|
|
1724
|
-
}));
|
|
1725
|
-
if (pubResult._tag === "Failure") {
|
|
1726
|
-
const err = pubResult.failure;
|
|
1727
|
-
console.error("[tablinum:publish] failed:", err);
|
|
1728
|
-
if (config.onSyncError)
|
|
1729
|
-
config.onSyncError(err);
|
|
1730
|
-
} else {
|
|
1731
|
-
console.log("[tablinum:publish] success");
|
|
1732
|
-
}
|
|
1733
|
-
});
|
|
1734
|
-
yield* Effect14.forkDetach(publishEffect);
|
|
1735
|
-
} else {
|
|
1736
|
-
const err = wrapResult.failure;
|
|
1737
|
-
console.error("[tablinum:onWrite] wrap failed:", err);
|
|
1738
|
-
if (config.onSyncError)
|
|
1739
|
-
config.onSyncError(err);
|
|
1740
|
-
}
|
|
1741
|
-
});
|
|
1742
|
-
const handles = new Map;
|
|
1743
|
-
for (const [, def] of schemaEntries) {
|
|
1744
|
-
const validator = buildValidator(def.name, def);
|
|
1745
|
-
const partialValidator = buildPartialValidator(def.name, def);
|
|
1746
|
-
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, onWrite);
|
|
1747
|
-
handles.set(def.name, handle);
|
|
1748
|
-
}
|
|
1749
|
-
yield* syncHandle.startSubscription();
|
|
1750
|
-
const dbHandle = {
|
|
1751
|
-
collection: (name) => {
|
|
1752
|
-
const handle = handles.get(name);
|
|
1753
|
-
if (!handle) {
|
|
1754
|
-
throw new Error(`Collection "${name}" not found in schema`);
|
|
1755
|
-
}
|
|
1756
|
-
return handle;
|
|
1757
|
-
},
|
|
1758
|
-
exportKey: () => identity.exportKey(),
|
|
1759
|
-
close: () => Effect14.gen(function* () {
|
|
1760
|
-
yield* Ref5.set(closedRef, true);
|
|
1761
|
-
yield* relayHandle.closeAll();
|
|
1762
|
-
yield* storage.close();
|
|
1763
|
-
}),
|
|
1764
|
-
rebuild: () => Effect14.gen(function* () {
|
|
1765
|
-
const closed = yield* Ref5.get(closedRef);
|
|
1766
|
-
if (closed) {
|
|
1767
|
-
return yield* new StorageError({ message: "Database is closed" });
|
|
1768
|
-
}
|
|
1769
|
-
yield* rebuild(storage, schemaEntries.map(([, def]) => def.name));
|
|
1770
|
-
}),
|
|
1771
|
-
sync: () => Effect14.gen(function* () {
|
|
1772
|
-
const closed = yield* Ref5.get(closedRef);
|
|
1773
|
-
if (closed) {
|
|
1774
|
-
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
1775
|
-
}
|
|
1776
|
-
yield* syncHandle.sync();
|
|
1777
|
-
}),
|
|
1778
|
-
getSyncStatus: () => syncStatus.get()
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
function createTablinum(config) {
|
|
2705
|
+
return Effect22.gen(function* () {
|
|
2706
|
+
yield* validateConfig(config);
|
|
2707
|
+
const runtimeConfig = yield* resolveRuntimeConfig(config);
|
|
2708
|
+
const configValue = {
|
|
2709
|
+
...runtimeConfig,
|
|
2710
|
+
schema: config.schema,
|
|
2711
|
+
onSyncError: config.onSyncError,
|
|
2712
|
+
onRemoved: config.onRemoved,
|
|
2713
|
+
onMembersChanged: config.onMembersChanged
|
|
1779
2714
|
};
|
|
1780
|
-
|
|
2715
|
+
const configLayer = Layer9.succeed(Config, configValue);
|
|
2716
|
+
const fullLayer = TablinumLive.pipe(Layer9.provide(configLayer));
|
|
2717
|
+
const ctx = yield* Layer9.build(fullLayer);
|
|
2718
|
+
return ServiceMap10.get(ctx, Tablinum);
|
|
1781
2719
|
});
|
|
1782
2720
|
}
|
|
1783
2721
|
|
|
1784
|
-
// src/svelte/database.svelte.ts
|
|
1785
|
-
import { Effect as Effect18, Scope as Scope3, Exit } from "effect";
|
|
1786
|
-
|
|
1787
2722
|
// src/svelte/collection.svelte.ts
|
|
1788
|
-
import { Effect as
|
|
2723
|
+
import { Effect as Effect24, Fiber, Option as Option11, Stream as Stream4 } from "effect";
|
|
1789
2724
|
|
|
1790
|
-
// src/svelte/
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2725
|
+
// src/svelte/deferred.ts
|
|
2726
|
+
function createDeferred() {
|
|
2727
|
+
let settled = false;
|
|
2728
|
+
let resolvePromise;
|
|
2729
|
+
let rejectPromise;
|
|
2730
|
+
const promise = new Promise((resolve, reject) => {
|
|
2731
|
+
resolvePromise = resolve;
|
|
2732
|
+
rejectPromise = reject;
|
|
2733
|
+
});
|
|
2734
|
+
return {
|
|
2735
|
+
promise,
|
|
2736
|
+
settled: () => settled,
|
|
2737
|
+
resolve: (value) => {
|
|
2738
|
+
if (settled)
|
|
2739
|
+
return;
|
|
2740
|
+
settled = true;
|
|
2741
|
+
resolvePromise(value);
|
|
2742
|
+
},
|
|
2743
|
+
reject: (reason) => {
|
|
2744
|
+
if (settled)
|
|
2745
|
+
return;
|
|
2746
|
+
settled = true;
|
|
2747
|
+
rejectPromise(reason);
|
|
1812
2748
|
}
|
|
1813
|
-
}
|
|
2749
|
+
};
|
|
1814
2750
|
}
|
|
1815
2751
|
|
|
1816
2752
|
// src/svelte/query.svelte.ts
|
|
1817
|
-
|
|
2753
|
+
import { Effect as Effect23, Option as Option10 } from "effect";
|
|
2754
|
+
function wrapQueryBuilder(getBuilder, touchVersion, ready) {
|
|
1818
2755
|
return {
|
|
1819
|
-
and: (fn) => wrapQueryBuilder(
|
|
1820
|
-
sortBy: (
|
|
1821
|
-
reverse: () => wrapQueryBuilder(
|
|
1822
|
-
offset: (n) => wrapQueryBuilder(
|
|
1823
|
-
limit: (n) => wrapQueryBuilder(
|
|
1824
|
-
get: () =>
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
return
|
|
2756
|
+
and: (fn) => wrapQueryBuilder(() => getBuilder().and(fn), touchVersion, ready),
|
|
2757
|
+
sortBy: (field2) => wrapQueryBuilder(() => getBuilder().sortBy(field2), touchVersion, ready),
|
|
2758
|
+
reverse: () => wrapQueryBuilder(() => getBuilder().reverse(), touchVersion, ready),
|
|
2759
|
+
offset: (n) => wrapQueryBuilder(() => getBuilder().offset(n), touchVersion, ready),
|
|
2760
|
+
limit: (n) => wrapQueryBuilder(() => getBuilder().limit(n), touchVersion, ready),
|
|
2761
|
+
get: () => {
|
|
2762
|
+
touchVersion();
|
|
2763
|
+
return ready.then(() => Effect23.runPromise(getBuilder().get()));
|
|
2764
|
+
},
|
|
2765
|
+
first: () => {
|
|
2766
|
+
touchVersion();
|
|
2767
|
+
return ready.then(() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull)));
|
|
2768
|
+
},
|
|
2769
|
+
count: () => {
|
|
2770
|
+
touchVersion();
|
|
2771
|
+
return ready.then(() => Effect23.runPromise(getBuilder().count()));
|
|
1831
2772
|
}
|
|
1832
2773
|
};
|
|
1833
2774
|
}
|
|
1834
|
-
function wrapWhereClause(
|
|
2775
|
+
function wrapWhereClause(getClause, touchVersion, ready) {
|
|
1835
2776
|
return {
|
|
1836
|
-
equals: (value) => wrapQueryBuilder(
|
|
1837
|
-
above: (value) => wrapQueryBuilder(
|
|
1838
|
-
aboveOrEqual: (value) => wrapQueryBuilder(
|
|
1839
|
-
below: (value) => wrapQueryBuilder(
|
|
1840
|
-
belowOrEqual: (value) => wrapQueryBuilder(
|
|
1841
|
-
between: (lower, upper, options) => wrapQueryBuilder(
|
|
1842
|
-
startsWith: (prefix) => wrapQueryBuilder(
|
|
1843
|
-
anyOf: (values) => wrapQueryBuilder(
|
|
1844
|
-
noneOf: (values) => wrapQueryBuilder(
|
|
2777
|
+
equals: (value) => wrapQueryBuilder(() => getClause().equals(value), touchVersion, ready),
|
|
2778
|
+
above: (value) => wrapQueryBuilder(() => getClause().above(value), touchVersion, ready),
|
|
2779
|
+
aboveOrEqual: (value) => wrapQueryBuilder(() => getClause().aboveOrEqual(value), touchVersion, ready),
|
|
2780
|
+
below: (value) => wrapQueryBuilder(() => getClause().below(value), touchVersion, ready),
|
|
2781
|
+
belowOrEqual: (value) => wrapQueryBuilder(() => getClause().belowOrEqual(value), touchVersion, ready),
|
|
2782
|
+
between: (lower, upper, options) => wrapQueryBuilder(() => getClause().between(lower, upper, options), touchVersion, ready),
|
|
2783
|
+
startsWith: (prefix) => wrapQueryBuilder(() => getClause().startsWith(prefix), touchVersion, ready),
|
|
2784
|
+
anyOf: (values) => wrapQueryBuilder(() => getClause().anyOf(values), touchVersion, ready),
|
|
2785
|
+
noneOf: (values) => wrapQueryBuilder(() => getClause().noneOf(values), touchVersion, ready)
|
|
1845
2786
|
};
|
|
1846
2787
|
}
|
|
1847
|
-
function wrapOrderByBuilder(
|
|
2788
|
+
function wrapOrderByBuilder(getBuilder, touchVersion, ready) {
|
|
1848
2789
|
return {
|
|
1849
|
-
reverse: () => wrapOrderByBuilder(
|
|
1850
|
-
offset: (n) => wrapOrderByBuilder(
|
|
1851
|
-
limit: (n) => wrapOrderByBuilder(
|
|
1852
|
-
get: () =>
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
return
|
|
2790
|
+
reverse: () => wrapOrderByBuilder(() => getBuilder().reverse(), touchVersion, ready),
|
|
2791
|
+
offset: (n) => wrapOrderByBuilder(() => getBuilder().offset(n), touchVersion, ready),
|
|
2792
|
+
limit: (n) => wrapOrderByBuilder(() => getBuilder().limit(n), touchVersion, ready),
|
|
2793
|
+
get: () => {
|
|
2794
|
+
touchVersion();
|
|
2795
|
+
return ready.then(() => Effect23.runPromise(getBuilder().get()));
|
|
2796
|
+
},
|
|
2797
|
+
first: () => {
|
|
2798
|
+
touchVersion();
|
|
2799
|
+
return ready.then(() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull)));
|
|
2800
|
+
},
|
|
2801
|
+
count: () => {
|
|
2802
|
+
touchVersion();
|
|
2803
|
+
return ready.then(() => Effect23.runPromise(getBuilder().count()));
|
|
1859
2804
|
}
|
|
1860
2805
|
};
|
|
1861
2806
|
}
|
|
1862
2807
|
|
|
1863
2808
|
// src/svelte/collection.svelte.ts
|
|
1864
2809
|
class Collection {
|
|
1865
|
-
items = $state([]);
|
|
1866
2810
|
error = $state(null);
|
|
1867
|
-
#handle;
|
|
2811
|
+
#handle = null;
|
|
2812
|
+
#ready = createDeferred();
|
|
2813
|
+
#version = $state(0);
|
|
2814
|
+
#watchAbort = null;
|
|
1868
2815
|
#watchFiber = null;
|
|
1869
|
-
|
|
1870
|
-
|
|
2816
|
+
_bind(handle) {
|
|
2817
|
+
if (this.#handle)
|
|
2818
|
+
return;
|
|
1871
2819
|
this.#handle = handle;
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
this.error = e instanceof Error ? e : new Error(String(e));
|
|
1876
|
-
})));
|
|
1877
|
-
this.#watchFiber = Effect17.runFork(watchEffect);
|
|
2820
|
+
this.error = null;
|
|
2821
|
+
this.#settleReady();
|
|
2822
|
+
this.#startWatch();
|
|
1878
2823
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2824
|
+
_fail(err) {
|
|
2825
|
+
this.error = err;
|
|
2826
|
+
this.#settleReady(err);
|
|
2827
|
+
}
|
|
2828
|
+
#settleReady(err) {
|
|
2829
|
+
if (err) {
|
|
2830
|
+
this.#ready.reject(err);
|
|
2831
|
+
} else {
|
|
2832
|
+
this.#ready.resolve();
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
#startWatch() {
|
|
2836
|
+
if (!this.#handle)
|
|
2837
|
+
return;
|
|
2838
|
+
const abort = new AbortController;
|
|
2839
|
+
this.#watchAbort = abort;
|
|
2840
|
+
this.#watchFiber = Effect24.runFork(Stream4.runForEach(this.#handle.watch(), (_records) => Effect24.sync(() => {
|
|
2841
|
+
if (!abort.signal.aborted) {
|
|
2842
|
+
this.#version++;
|
|
2843
|
+
}
|
|
2844
|
+
})).pipe(Effect24.catch((e) => Effect24.sync(() => {
|
|
2845
|
+
if (!abort.signal.aborted) {
|
|
2846
|
+
this.error = e instanceof Error ? e : new Error(String(e));
|
|
2847
|
+
}
|
|
2848
|
+
}))));
|
|
2849
|
+
}
|
|
2850
|
+
#touchVersion = () => {
|
|
2851
|
+
this.#version;
|
|
2852
|
+
};
|
|
2853
|
+
#handleOrThrow = () => {
|
|
2854
|
+
if (this.#handle)
|
|
2855
|
+
return this.#handle;
|
|
2856
|
+
throw this.error ?? new ClosedError({ message: "Collection is not ready" });
|
|
2857
|
+
};
|
|
2858
|
+
#run = async (getEffect) => {
|
|
2859
|
+
await this.#ready.promise;
|
|
2860
|
+
return Effect24.runPromise(getEffect());
|
|
2861
|
+
};
|
|
2862
|
+
add = (data) => {
|
|
2863
|
+
return this.#run(() => this.#handleOrThrow().add(data));
|
|
2864
|
+
};
|
|
2865
|
+
update = (id, data) => {
|
|
2866
|
+
return this.#run(() => this.#handleOrThrow().update(id, data));
|
|
2867
|
+
};
|
|
2868
|
+
delete = (id) => {
|
|
2869
|
+
return this.#run(() => this.#handleOrThrow().delete(id));
|
|
2870
|
+
};
|
|
2871
|
+
undo = (id) => {
|
|
2872
|
+
return this.#run(() => this.#handleOrThrow().undo(id));
|
|
2873
|
+
};
|
|
2874
|
+
get(id) {
|
|
2875
|
+
if (typeof id === "string") {
|
|
2876
|
+
return this.#run(() => this.#handleOrThrow().get(id));
|
|
1886
2877
|
}
|
|
2878
|
+
this.#touchVersion();
|
|
2879
|
+
return this.#run(() => Stream4.runHead(this.#handleOrThrow().watch()).pipe(Effect24.map((opt) => Option11.getOrElse(opt, () => []))));
|
|
2880
|
+
}
|
|
2881
|
+
first = () => {
|
|
2882
|
+
this.#touchVersion();
|
|
2883
|
+
return this.#run(() => Effect24.map(this.#handleOrThrow().first(), Option11.getOrNull));
|
|
1887
2884
|
};
|
|
1888
|
-
|
|
1889
|
-
this.#
|
|
2885
|
+
count = () => {
|
|
2886
|
+
this.#touchVersion();
|
|
2887
|
+
return this.#run(() => this.#handleOrThrow().count());
|
|
1890
2888
|
};
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
delete = (id) => this.#run(this.#handle.delete(id));
|
|
1894
|
-
get = (id) => this.#run(this.#handle.get(id));
|
|
1895
|
-
first = () => this.#run(this.#handle.first());
|
|
1896
|
-
count = () => this.#run(this.#handle.count());
|
|
1897
|
-
where = (field) => {
|
|
1898
|
-
return wrapWhereClause(this.#handle.where(field), this.#onLive);
|
|
2889
|
+
where = (field2) => {
|
|
2890
|
+
return wrapWhereClause(() => this.#handleOrThrow().where(field2), this.#touchVersion, this.#ready.promise);
|
|
1899
2891
|
};
|
|
1900
|
-
orderBy = (
|
|
1901
|
-
return wrapOrderByBuilder(this.#
|
|
2892
|
+
orderBy = (field2) => {
|
|
2893
|
+
return wrapOrderByBuilder(() => this.#handleOrThrow().orderBy(field2), this.#touchVersion, this.#ready.promise);
|
|
1902
2894
|
};
|
|
1903
|
-
_destroy() {
|
|
2895
|
+
_destroy(reason = new ClosedError({ message: "Collection is closed" })) {
|
|
2896
|
+
if (this.#watchAbort) {
|
|
2897
|
+
this.#watchAbort.abort();
|
|
2898
|
+
this.#watchAbort = null;
|
|
2899
|
+
}
|
|
1904
2900
|
if (this.#watchFiber) {
|
|
1905
|
-
|
|
2901
|
+
Effect24.runFork(Fiber.interrupt(this.#watchFiber));
|
|
1906
2902
|
this.#watchFiber = null;
|
|
1907
2903
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
this.#liveQueries.clear();
|
|
2904
|
+
this.#handle = null;
|
|
2905
|
+
this.error ??= reason;
|
|
2906
|
+
this.#settleReady(this.error);
|
|
1912
2907
|
}
|
|
1913
2908
|
}
|
|
1914
2909
|
|
|
1915
|
-
// src/svelte/
|
|
1916
|
-
class
|
|
1917
|
-
status = $state("
|
|
2910
|
+
// src/svelte/tablinum.svelte.ts
|
|
2911
|
+
class Tablinum2 {
|
|
2912
|
+
status = $state("initializing");
|
|
2913
|
+
syncStatus = $state("idle");
|
|
2914
|
+
pendingCount = $state(0);
|
|
2915
|
+
relayStatus = $state({ connectedUrls: [] });
|
|
1918
2916
|
error = $state(null);
|
|
1919
|
-
|
|
1920
|
-
#
|
|
2917
|
+
ready;
|
|
2918
|
+
#handle = null;
|
|
2919
|
+
#scope = null;
|
|
1921
2920
|
#collections = new Map;
|
|
1922
|
-
#
|
|
2921
|
+
#members = new Collection;
|
|
2922
|
+
#unsubscribeSyncStatus = null;
|
|
2923
|
+
#unsubscribePendingCount = null;
|
|
2924
|
+
#unsubscribeRelayStatus = null;
|
|
1923
2925
|
#closed = false;
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
this
|
|
1927
|
-
this.#
|
|
1928
|
-
if (this.#closed)
|
|
1929
|
-
return;
|
|
1930
|
-
Effect18.runPromise(this.#handle.getSyncStatus()).then((s) => {
|
|
1931
|
-
this.status = s;
|
|
1932
|
-
}).catch(() => {});
|
|
1933
|
-
}, 1000);
|
|
2926
|
+
#readyState = createDeferred();
|
|
2927
|
+
constructor(config) {
|
|
2928
|
+
this.ready = this.#readyState.promise;
|
|
2929
|
+
this.#init(config);
|
|
1934
2930
|
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
this.#collections.set(name, col);
|
|
2931
|
+
#settleReady(err) {
|
|
2932
|
+
if (err) {
|
|
2933
|
+
this.#readyState.reject(err);
|
|
2934
|
+
} else {
|
|
2935
|
+
this.#readyState.resolve();
|
|
1941
2936
|
}
|
|
1942
|
-
return col;
|
|
1943
|
-
}
|
|
1944
|
-
exportKey() {
|
|
1945
|
-
return this.#handle.exportKey();
|
|
1946
2937
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
if (this.#statusInterval) {
|
|
1952
|
-
clearInterval(this.#statusInterval);
|
|
1953
|
-
this.#statusInterval = null;
|
|
1954
|
-
}
|
|
1955
|
-
for (const col of this.#collections.values()) {
|
|
1956
|
-
col._destroy();
|
|
2938
|
+
#bindCollections(handle) {
|
|
2939
|
+
this.#members._bind(handle.members);
|
|
2940
|
+
for (const [name, collection2] of this.#collections) {
|
|
2941
|
+
collection2._bind(handle.collection(name));
|
|
1957
2942
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
};
|
|
1962
|
-
sync = async () => {
|
|
2943
|
+
}
|
|
2944
|
+
#runHandleEffect = async (run) => {
|
|
2945
|
+
const handle = this.#requireReady();
|
|
1963
2946
|
try {
|
|
1964
2947
|
this.error = null;
|
|
1965
|
-
await
|
|
2948
|
+
return await Effect25.runPromise(run(handle));
|
|
1966
2949
|
} catch (e) {
|
|
1967
2950
|
this.error = e instanceof Error ? e : new Error(String(e));
|
|
1968
2951
|
throw this.error;
|
|
1969
2952
|
}
|
|
1970
2953
|
};
|
|
1971
|
-
|
|
2954
|
+
async#init(config) {
|
|
2955
|
+
const scope = Effect25.runSync(Scope6.make());
|
|
1972
2956
|
try {
|
|
1973
|
-
|
|
1974
|
-
|
|
2957
|
+
const handle = await Effect25.runPromise(createTablinum(config).pipe(Effect25.provideService(Scope6.Scope, scope)));
|
|
2958
|
+
if (this.#closed) {
|
|
2959
|
+
await Effect25.runPromise(Scope6.close(scope, Exit2.void));
|
|
2960
|
+
this.#scope = null;
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
this.#handle = handle;
|
|
2964
|
+
this.#scope = scope;
|
|
2965
|
+
this.#bindCollections(handle);
|
|
2966
|
+
this.#unsubscribeSyncStatus = handle.subscribeSyncStatus((s) => {
|
|
2967
|
+
this.syncStatus = s;
|
|
2968
|
+
});
|
|
2969
|
+
this.#unsubscribePendingCount = handle.subscribePendingCount((count) => {
|
|
2970
|
+
this.pendingCount = count;
|
|
2971
|
+
});
|
|
2972
|
+
this.pendingCount = await Effect25.runPromise(handle.pendingCount());
|
|
2973
|
+
this.#unsubscribeRelayStatus = handle.subscribeRelayStatus((s) => {
|
|
2974
|
+
this.relayStatus = s;
|
|
2975
|
+
});
|
|
2976
|
+
this.relayStatus = handle.getRelayStatus();
|
|
2977
|
+
this.status = "ready";
|
|
2978
|
+
this.#settleReady();
|
|
1975
2979
|
} catch (e) {
|
|
1976
|
-
|
|
1977
|
-
|
|
2980
|
+
await Effect25.runPromise(Scope6.close(scope, Exit2.fail(e))).catch(() => {});
|
|
2981
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
2982
|
+
this.error = err;
|
|
2983
|
+
this.status = "error";
|
|
2984
|
+
this.#members._fail(err);
|
|
2985
|
+
for (const col of this.#collections.values()) {
|
|
2986
|
+
col._fail(err);
|
|
2987
|
+
}
|
|
2988
|
+
this.#settleReady(err);
|
|
1978
2989
|
}
|
|
1979
|
-
};
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
// src/schema/field.ts
|
|
1983
|
-
function make(kind, isOptional, isArray) {
|
|
1984
|
-
return { _tag: "FieldDef", kind, isOptional, isArray };
|
|
1985
|
-
}
|
|
1986
|
-
var field = {
|
|
1987
|
-
string: () => make("string", false, false),
|
|
1988
|
-
number: () => make("number", false, false),
|
|
1989
|
-
boolean: () => make("boolean", false, false),
|
|
1990
|
-
json: () => make("json", false, false),
|
|
1991
|
-
optional: (inner) => make(inner.kind, true, inner.isArray),
|
|
1992
|
-
array: (inner) => make(inner.kind, inner.isOptional, true)
|
|
1993
|
-
};
|
|
1994
|
-
// src/schema/collection.ts
|
|
1995
|
-
var RESERVED_NAMES = new Set(["id", "_deleted", "_createdAt", "_updatedAt"]);
|
|
1996
|
-
function collection(name, fields, options) {
|
|
1997
|
-
if (!name || name.trim().length === 0) {
|
|
1998
|
-
throw new Error("Collection name must not be empty");
|
|
1999
2990
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
throw new Error(`Collection "${name}" must have at least one field`);
|
|
2991
|
+
get publicKey() {
|
|
2992
|
+
return this.#handle?.publicKey ?? "";
|
|
2003
2993
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
throw new Error(`Field name "${fieldName}" is reserved`);
|
|
2007
|
-
}
|
|
2994
|
+
get members() {
|
|
2995
|
+
return this.#members;
|
|
2008
2996
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2997
|
+
collection(name) {
|
|
2998
|
+
if (this.#closed) {
|
|
2999
|
+
throw new ClosedError({ message: "Tablinum is closed" });
|
|
3000
|
+
}
|
|
3001
|
+
let col = this.#collections.get(name);
|
|
3002
|
+
if (!col) {
|
|
3003
|
+
col = new Collection;
|
|
3004
|
+
this.#collections.set(name, col);
|
|
3005
|
+
if (this.#handle) {
|
|
3006
|
+
col._bind(this.#handle.collection(name));
|
|
2018
3007
|
}
|
|
2019
|
-
indices.push(idx);
|
|
2020
3008
|
}
|
|
3009
|
+
return col;
|
|
2021
3010
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
throw e;
|
|
3011
|
+
#requireReady() {
|
|
3012
|
+
if (!this.#handle) {
|
|
3013
|
+
throw this.error ?? new ClosedError({ message: "Tablinum is not ready" });
|
|
3014
|
+
}
|
|
3015
|
+
return this.#handle;
|
|
3016
|
+
}
|
|
3017
|
+
exportKey() {
|
|
3018
|
+
return this.#requireReady().exportKey();
|
|
3019
|
+
}
|
|
3020
|
+
exportInvite() {
|
|
3021
|
+
return this.#requireReady().exportInvite();
|
|
2034
3022
|
}
|
|
3023
|
+
close = async () => {
|
|
3024
|
+
if (this.#closed)
|
|
3025
|
+
return;
|
|
3026
|
+
this.#closed = true;
|
|
3027
|
+
const closeError = new ClosedError({
|
|
3028
|
+
message: this.#readyState.settled() ? "Tablinum is closed" : "Tablinum was closed before initialization completed"
|
|
3029
|
+
});
|
|
3030
|
+
if (this.#unsubscribeSyncStatus) {
|
|
3031
|
+
this.#unsubscribeSyncStatus();
|
|
3032
|
+
this.#unsubscribeSyncStatus = null;
|
|
3033
|
+
}
|
|
3034
|
+
if (this.#unsubscribePendingCount) {
|
|
3035
|
+
this.#unsubscribePendingCount();
|
|
3036
|
+
this.#unsubscribePendingCount = null;
|
|
3037
|
+
}
|
|
3038
|
+
if (this.#unsubscribeRelayStatus) {
|
|
3039
|
+
this.#unsubscribeRelayStatus();
|
|
3040
|
+
this.#unsubscribeRelayStatus = null;
|
|
3041
|
+
}
|
|
3042
|
+
this.#members._destroy(closeError);
|
|
3043
|
+
for (const col of this.#collections.values())
|
|
3044
|
+
col._destroy(closeError);
|
|
3045
|
+
this.#collections.clear();
|
|
3046
|
+
const handle = this.#handle;
|
|
3047
|
+
const scope = this.#scope;
|
|
3048
|
+
this.#handle = null;
|
|
3049
|
+
this.#scope = null;
|
|
3050
|
+
if (handle && scope) {
|
|
3051
|
+
await Effect25.runPromise(handle.close());
|
|
3052
|
+
await Effect25.runPromise(Scope6.close(scope, Exit2.void));
|
|
3053
|
+
}
|
|
3054
|
+
if (!this.#readyState.settled()) {
|
|
3055
|
+
this.error = closeError;
|
|
3056
|
+
this.#settleReady(closeError);
|
|
3057
|
+
}
|
|
3058
|
+
this.status = "closed";
|
|
3059
|
+
};
|
|
3060
|
+
sync = async () => this.#runHandleEffect((handle) => handle.sync());
|
|
3061
|
+
rebuild = async () => this.#runHandleEffect((handle) => handle.rebuild());
|
|
3062
|
+
addMember = async (pubkey) => this.#runHandleEffect((handle) => handle.addMember(pubkey));
|
|
3063
|
+
removeMember = async (pubkey) => this.#runHandleEffect((handle) => handle.removeMember(pubkey));
|
|
3064
|
+
getMembers = async () => this.#runHandleEffect((handle) => handle.getMembers());
|
|
3065
|
+
getProfile = async () => this.#runHandleEffect((handle) => handle.getProfile());
|
|
3066
|
+
setProfile = async (profile) => this.#runHandleEffect((handle) => handle.setProfile(profile));
|
|
2035
3067
|
}
|
|
2036
3068
|
export {
|
|
2037
3069
|
field,
|
|
2038
|
-
|
|
3070
|
+
encodeInvite,
|
|
3071
|
+
decodeInvite,
|
|
2039
3072
|
collection,
|
|
2040
3073
|
ValidationError,
|
|
3074
|
+
Tablinum2 as Tablinum,
|
|
2041
3075
|
SyncError,
|
|
2042
3076
|
StorageError,
|
|
2043
3077
|
RelayError,
|
|
2044
3078
|
NotFoundError,
|
|
2045
|
-
LiveQuery,
|
|
2046
|
-
Database,
|
|
2047
3079
|
CryptoError,
|
|
2048
3080
|
Collection,
|
|
2049
3081
|
ClosedError
|