@ebbjs/storage 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +202 -0
- package/README.md +83 -2
- package/dist/cursor-record-D39emd6R.js +21 -0
- package/dist/indexeddb/index.d.ts +465 -0
- package/dist/indexeddb/index.js +335 -0
- package/dist/memory/index.d.ts +474 -0
- package/dist/memory/index.js +296 -0
- package/dist/replica/index.d.ts +271 -0
- package/dist/replica/index.js +308 -0
- package/dist/replica-record-D3lkFACh.js +167 -0
- package/dist/types/index.d.ts +621 -0
- package/dist/types/index.js +1 -0
- package/package.json +78 -3
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { m as b, a as E, i as M, c as m } from "../cursor-record-D39emd6R.js";
|
|
2
|
+
import { l as D, c as _, a as A, f as T, b as h, h as w, k as j, j as v, n as R, M as f, m as k, p as I, r as F, i as q } from "../replica-record-D3lkFACh.js";
|
|
3
|
+
import { compare as C } from "@ebbjs/core";
|
|
4
|
+
const O = (t, n) => {
|
|
5
|
+
const e = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const [s, a] of t)
|
|
7
|
+
e.set(s, new Set(a));
|
|
8
|
+
for (const s of n.updates) {
|
|
9
|
+
const a = e.get(s.subject_type);
|
|
10
|
+
a ? a.add(s.subject_id) : e.set(s.subject_type, /* @__PURE__ */ new Set([s.subject_id]));
|
|
11
|
+
}
|
|
12
|
+
return Array.from(e.entries()).map(([s, a]) => [s, Array.from(a)]);
|
|
13
|
+
}, P = () => {
|
|
14
|
+
let t = {
|
|
15
|
+
actions: [],
|
|
16
|
+
typeIndex: []
|
|
17
|
+
};
|
|
18
|
+
const n = async (e) => {
|
|
19
|
+
const s = t.actions.findIndex((d) => d.id === e.id), a = s === -1 ? void 0 : t.actions[s], o = a === void 0 ? e : { ...a, updates: b(a.updates, e.updates) }, c = a === void 0, r = c ? t.actions.length + 1 : s + 1, i = E(a?.updates ?? [], e.updates);
|
|
20
|
+
return t = {
|
|
21
|
+
actions: c ? [...t.actions, o] : t.actions.map((d) => d.id === e.id ? o : d),
|
|
22
|
+
typeIndex: O(t.typeIndex, o)
|
|
23
|
+
}, { action: o, position: r, added: i };
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
append: n,
|
|
27
|
+
async appendMany(e) {
|
|
28
|
+
const s = [];
|
|
29
|
+
for (const a of e) s.push(await n(a));
|
|
30
|
+
return s;
|
|
31
|
+
},
|
|
32
|
+
async readTail(e) {
|
|
33
|
+
return t.actions.slice(e).map((s, a) => ({
|
|
34
|
+
action: s,
|
|
35
|
+
position: e + a + 1,
|
|
36
|
+
added: s.updates
|
|
37
|
+
}));
|
|
38
|
+
},
|
|
39
|
+
async lastPosition() {
|
|
40
|
+
return t.actions.length;
|
|
41
|
+
},
|
|
42
|
+
async getAll() {
|
|
43
|
+
return t.actions;
|
|
44
|
+
},
|
|
45
|
+
async getForEntity(e) {
|
|
46
|
+
return t.actions.filter(
|
|
47
|
+
(a) => a.updates.some((o) => o.subject_id === e)
|
|
48
|
+
).sort((a, o) => a.gsn - o.gsn);
|
|
49
|
+
},
|
|
50
|
+
async clear() {
|
|
51
|
+
t = { actions: [], typeIndex: [] };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}, B = () => {
|
|
55
|
+
let t = {
|
|
56
|
+
entities: {},
|
|
57
|
+
typeIndex: {}
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
async mark(n, e) {
|
|
61
|
+
n in t.entities || (t = {
|
|
62
|
+
entities: { ...t.entities, [n]: e },
|
|
63
|
+
typeIndex: {
|
|
64
|
+
...t.typeIndex,
|
|
65
|
+
[e]: new Set(t.typeIndex[e] ?? []).add(n)
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
async isDirty(n) {
|
|
70
|
+
return n in t.entities;
|
|
71
|
+
},
|
|
72
|
+
async getDirtyForType(n) {
|
|
73
|
+
const e = t.typeIndex[n];
|
|
74
|
+
return e ? [...e] : [];
|
|
75
|
+
},
|
|
76
|
+
async clear(n) {
|
|
77
|
+
const e = t.entities[n];
|
|
78
|
+
if (!e) return;
|
|
79
|
+
const { [n]: s, ...a } = t.entities, o = t.typeIndex[e], c = new Set(o);
|
|
80
|
+
c.delete(n), t = {
|
|
81
|
+
entities: a,
|
|
82
|
+
typeIndex: {
|
|
83
|
+
...t.typeIndex,
|
|
84
|
+
[e]: c
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
async clearAll() {
|
|
89
|
+
t = { entities: {}, typeIndex: {} };
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}, u = (t) => JSON.parse(JSON.stringify(t)), g = (t, n, e) => {
|
|
93
|
+
const s = { ...t };
|
|
94
|
+
if (e && e !== n.type) {
|
|
95
|
+
const a = new Set(s[e] ?? []);
|
|
96
|
+
a.delete(n.id), s[e] = a;
|
|
97
|
+
}
|
|
98
|
+
return s[n.type] || (s[n.type] = /* @__PURE__ */ new Set()), s[n.type] = new Set(s[n.type]).add(n.id), s;
|
|
99
|
+
}, S = (t, n, e) => w(
|
|
100
|
+
w(t, v(n, e)),
|
|
101
|
+
j(n, e)
|
|
102
|
+
), L = (t, n) => {
|
|
103
|
+
let e = { entities: {}, typeIndex: {}, index: {} };
|
|
104
|
+
const s = async (r) => {
|
|
105
|
+
if (!await n.isDirty(r)) return;
|
|
106
|
+
const d = await t.getForEntity(r);
|
|
107
|
+
if (d.length === 0) return;
|
|
108
|
+
let y = null;
|
|
109
|
+
for (const p of d)
|
|
110
|
+
for (const x of p.updates)
|
|
111
|
+
x.subject_id === r && (y = R(y, x, p.gsn, p.hlc));
|
|
112
|
+
if (y === null) return;
|
|
113
|
+
const l = e.entities[r];
|
|
114
|
+
e = {
|
|
115
|
+
entities: { ...e.entities, [r]: u(y) },
|
|
116
|
+
typeIndex: g(e.typeIndex, y, l?.type),
|
|
117
|
+
index: S(e.index, l, y)
|
|
118
|
+
}, await n.clear(r);
|
|
119
|
+
}, a = async (r) => {
|
|
120
|
+
for (const i of await n.getDirtyForType(r))
|
|
121
|
+
await s(i);
|
|
122
|
+
}, o = (r) => h(e.index[r]);
|
|
123
|
+
return {
|
|
124
|
+
async get(r) {
|
|
125
|
+
await s(r);
|
|
126
|
+
const i = e.entities[r];
|
|
127
|
+
return i ? u(i) : null;
|
|
128
|
+
},
|
|
129
|
+
async set(r) {
|
|
130
|
+
const i = e.entities[r.id];
|
|
131
|
+
e = {
|
|
132
|
+
entities: { ...e.entities, [r.id]: u(r) },
|
|
133
|
+
typeIndex: g(e.typeIndex, r, i?.type),
|
|
134
|
+
index: S(e.index, i, r)
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
async query(r) {
|
|
138
|
+
return await a(r), [...e.typeIndex[r] ?? /* @__PURE__ */ new Set()].map((d) => u(e.entities[d]));
|
|
139
|
+
},
|
|
140
|
+
async queryByRelationship({
|
|
141
|
+
as: r,
|
|
142
|
+
type: i,
|
|
143
|
+
targetId: d
|
|
144
|
+
}) {
|
|
145
|
+
return await a(I), o(F(r, i, d));
|
|
146
|
+
},
|
|
147
|
+
async queryByRelationshipSource({
|
|
148
|
+
as: r,
|
|
149
|
+
type: i,
|
|
150
|
+
sourceId: d
|
|
151
|
+
}) {
|
|
152
|
+
return await a(I), A(e.index[T(r, i, d)]);
|
|
153
|
+
},
|
|
154
|
+
async queryByMembership(r) {
|
|
155
|
+
return await a(f), o(k(r));
|
|
156
|
+
},
|
|
157
|
+
async queryByMembershipEntity(r) {
|
|
158
|
+
return await a(f), D(e.index[_(r)]);
|
|
159
|
+
},
|
|
160
|
+
async reset() {
|
|
161
|
+
e = { entities: {}, typeIndex: {}, index: {} };
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}, N = () => {
|
|
165
|
+
let t = { records: {} };
|
|
166
|
+
const n = (e) => t.records[e] ?? null;
|
|
167
|
+
return {
|
|
168
|
+
async get(e) {
|
|
169
|
+
return n(e)?.cursor ?? null;
|
|
170
|
+
},
|
|
171
|
+
async set(e, s) {
|
|
172
|
+
t = {
|
|
173
|
+
records: { ...t.records, [e]: m({ ...n(e), cursor: s }) }
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
async getRecord(e) {
|
|
177
|
+
return n(e);
|
|
178
|
+
},
|
|
179
|
+
async setRecord(e, s) {
|
|
180
|
+
if (M(s)) {
|
|
181
|
+
const { [e]: a, ...o } = t.records;
|
|
182
|
+
t = { records: o };
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
t = { records: { ...t.records, [e]: m(s) } };
|
|
186
|
+
},
|
|
187
|
+
async records() {
|
|
188
|
+
return new Map(Object.entries(t.records));
|
|
189
|
+
},
|
|
190
|
+
async clear() {
|
|
191
|
+
t = { records: {} };
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}, H = () => {
|
|
195
|
+
let t = {};
|
|
196
|
+
return {
|
|
197
|
+
async put(n) {
|
|
198
|
+
t = { ...t, [n.action.id]: structuredClone(n) };
|
|
199
|
+
},
|
|
200
|
+
async putAndDelete(n, e) {
|
|
201
|
+
const { [e]: s, ...a } = t;
|
|
202
|
+
t = { ...a, [n.action.id]: structuredClone(n) };
|
|
203
|
+
},
|
|
204
|
+
async list() {
|
|
205
|
+
return Object.values(t).map((n) => structuredClone(n)).sort((n, e) => C(n.enqueuedAtHlc, e.enqueuedAtHlc));
|
|
206
|
+
},
|
|
207
|
+
async get(n) {
|
|
208
|
+
const e = t[n];
|
|
209
|
+
return e === void 0 ? null : structuredClone(e);
|
|
210
|
+
},
|
|
211
|
+
async delete(n) {
|
|
212
|
+
const { [n]: e, ...s } = t;
|
|
213
|
+
t = s;
|
|
214
|
+
},
|
|
215
|
+
async clear() {
|
|
216
|
+
t = {};
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}, K = () => {
|
|
220
|
+
let t = null;
|
|
221
|
+
return {
|
|
222
|
+
async load() {
|
|
223
|
+
return q(t) ? t : null;
|
|
224
|
+
},
|
|
225
|
+
async save(n) {
|
|
226
|
+
t = n;
|
|
227
|
+
},
|
|
228
|
+
async clear() {
|
|
229
|
+
t = null;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}, z = () => {
|
|
233
|
+
const t = P(), n = B(), e = L(t, n), s = N(), a = H(), o = K();
|
|
234
|
+
return {
|
|
235
|
+
actions: {
|
|
236
|
+
async append(c) {
|
|
237
|
+
const r = await t.append(c);
|
|
238
|
+
for (const i of c.updates)
|
|
239
|
+
await n.mark(i.subject_id, i.subject_type);
|
|
240
|
+
return r;
|
|
241
|
+
},
|
|
242
|
+
async appendMany(c) {
|
|
243
|
+
const r = await t.appendMany(c);
|
|
244
|
+
for (const i of c)
|
|
245
|
+
for (const d of i.updates)
|
|
246
|
+
await n.mark(d.subject_id, d.subject_type);
|
|
247
|
+
return r;
|
|
248
|
+
},
|
|
249
|
+
async readTail(c) {
|
|
250
|
+
return t.readTail(c);
|
|
251
|
+
},
|
|
252
|
+
async lastPosition() {
|
|
253
|
+
return t.lastPosition();
|
|
254
|
+
},
|
|
255
|
+
async getAll() {
|
|
256
|
+
return t.getAll();
|
|
257
|
+
},
|
|
258
|
+
async getForEntity(c) {
|
|
259
|
+
return t.getForEntity(c);
|
|
260
|
+
},
|
|
261
|
+
async clear() {
|
|
262
|
+
await t.clear(), await n.clearAll(), await e.reset(), await o.clear();
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
entities: e,
|
|
266
|
+
dirtyTracker: {
|
|
267
|
+
async mark(c, r) {
|
|
268
|
+
await n.mark(c, r);
|
|
269
|
+
},
|
|
270
|
+
async isDirty(c) {
|
|
271
|
+
return n.isDirty(c);
|
|
272
|
+
},
|
|
273
|
+
async getDirtyForType(c) {
|
|
274
|
+
return n.getDirtyForType(c);
|
|
275
|
+
},
|
|
276
|
+
async clear(c) {
|
|
277
|
+
await n.clear(c);
|
|
278
|
+
},
|
|
279
|
+
async clearAll() {
|
|
280
|
+
await n.clearAll();
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
cursors: s,
|
|
284
|
+
outbox: a,
|
|
285
|
+
replica: o,
|
|
286
|
+
async isDirty(c) {
|
|
287
|
+
return n.isDirty(c);
|
|
288
|
+
},
|
|
289
|
+
async reset() {
|
|
290
|
+
await t.clear(), await n.clearAll(), await e.reset(), await o.clear(), await s.clear();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
export {
|
|
295
|
+
z as createMemoryAdapter
|
|
296
|
+
};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { Entity, Update, HLCTimestamp, Action } from '@ebbjs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a materialized row keeps large map fields in the replica's sharded
|
|
5
|
+
* container. The replica owns its rows and flattens them at the record
|
|
6
|
+
* boundary; the legacy adapters deep-clone every row, so they keep the plain
|
|
7
|
+
* wire shape.
|
|
8
|
+
*/
|
|
9
|
+
interface MaterializeOptions {
|
|
10
|
+
readonly residentMapFields?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Applies a single update to an entity during materialization.
|
|
14
|
+
*
|
|
15
|
+
* - PUT: full replacement
|
|
16
|
+
* - PATCH: field-level LWW merge; ignored on a soft-deleted entity
|
|
17
|
+
* - DELETE: soft delete (sets `deleted_hlc`); bumps `last_gsn` and
|
|
18
|
+
* `updated_hlc`. A delete for a row that was never materialized is a
|
|
19
|
+
* no-op, so a per-Update delivery (#473) cannot break the drain.
|
|
20
|
+
*/
|
|
21
|
+
declare const applyUpdate: (entity: Entity | null, update: Update, gsn: number, hlc: HLCTimestamp, options?: MaterializeOptions) => Entity | null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True when `next` differs from `prev` in anything a reader can observe.
|
|
25
|
+
* `last_gsn` is delivery bookkeeping and is excluded, so a lower-HLC patch
|
|
26
|
+
* that only advances it leaves the revision untouched.
|
|
27
|
+
*/
|
|
28
|
+
declare const contentChanged: (prev: Entity | null | undefined, next: Entity | null | undefined) => boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Lookup key for the source-keyed forward relationship index: the
|
|
32
|
+
* accessor name (`as`), the relationship type string, and the source
|
|
33
|
+
* entity id.
|
|
34
|
+
*/
|
|
35
|
+
interface RelationshipSourceQuery {
|
|
36
|
+
readonly as: string;
|
|
37
|
+
readonly type: string;
|
|
38
|
+
readonly sourceId: string;
|
|
39
|
+
}
|
|
40
|
+
/** One live relationship edge read from the source side. */
|
|
41
|
+
interface RelationshipLink {
|
|
42
|
+
/** The `relationship` row id — the `subject_id` an unlink names. */
|
|
43
|
+
readonly relationshipId: string;
|
|
44
|
+
readonly targetId: string;
|
|
45
|
+
}
|
|
46
|
+
/** One live membership row read from the member entity side. */
|
|
47
|
+
interface MembershipLink {
|
|
48
|
+
/** The `entityGroup` row id — the `subject_id` a delete names. */
|
|
49
|
+
readonly membershipId: string;
|
|
50
|
+
readonly groupId: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The persisted form of the materialized replica — the replica at rest.
|
|
55
|
+
*
|
|
56
|
+
* `position` is the `ActionLog` append cursor the record was taken at:
|
|
57
|
+
* load is `record ⊕ readTail(record.position)`. `revision` is the
|
|
58
|
+
* replica's observable-content version at that point. `entities` is the
|
|
59
|
+
* row-shaped entity set; derived indices are rebuilt on load, never
|
|
60
|
+
* persisted, because IndexedDB cannot index nested `FieldValue` paths,
|
|
61
|
+
* express live-row filtering, or query a stored map.
|
|
62
|
+
*/
|
|
63
|
+
interface ReplicaRecord {
|
|
64
|
+
/** Shape version of this record. A record of an unknown format reads as absent. */
|
|
65
|
+
readonly format: number;
|
|
66
|
+
/** The log position the record was taken at. */
|
|
67
|
+
readonly position: number;
|
|
68
|
+
/** The replica's revision when the record was taken. */
|
|
69
|
+
readonly revision: number;
|
|
70
|
+
readonly entities: readonly Entity[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A monotonic counter over the replica's observable content. It bumps once
|
|
75
|
+
* per entity change a reader could see — never for delivery bookkeeping like
|
|
76
|
+
* `last_gsn` — so it doubles as the invalidation token for derived state.
|
|
77
|
+
*/
|
|
78
|
+
type Revision = number;
|
|
79
|
+
/** One observable entity change, appended to the revision journal. */
|
|
80
|
+
interface ReplicaChange {
|
|
81
|
+
readonly revision: Revision;
|
|
82
|
+
readonly id: string;
|
|
83
|
+
readonly type: string;
|
|
84
|
+
readonly deleted: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether the replica retains a type's rows. The seam #500 builds on-demand
|
|
88
|
+
* residency against: a policy that holds nothing lets the engine fetch a type
|
|
89
|
+
* on first read and react to the journal it still receives.
|
|
90
|
+
*/
|
|
91
|
+
interface MaterializationPolicy {
|
|
92
|
+
holds(type: string): boolean;
|
|
93
|
+
}
|
|
94
|
+
interface ReplicaOptions {
|
|
95
|
+
readonly materialization?: MaterializationPolicy;
|
|
96
|
+
/**
|
|
97
|
+
* Reads a row from the local stores when `open` misses. Without it an
|
|
98
|
+
* on-demand id that is not resident stays a miss.
|
|
99
|
+
*/
|
|
100
|
+
readonly readEntity?: (id: string) => Promise<Entity | null>;
|
|
101
|
+
/**
|
|
102
|
+
* Maximum resident on-demand rows. Defaults to 16. A non-positive or
|
|
103
|
+
* non-finite value keeps every opened row resident; opening past the
|
|
104
|
+
* window releases the least recently opened one.
|
|
105
|
+
*/
|
|
106
|
+
readonly pinnedCapacity?: number;
|
|
107
|
+
/**
|
|
108
|
+
* How long a closed on-demand row stays cached before release. Defaults to
|
|
109
|
+
* 30_000 ms. A non-positive or non-finite value releases it immediately on
|
|
110
|
+
* close. An open row is never released by the TTL.
|
|
111
|
+
*/
|
|
112
|
+
readonly pinnedTtlMs?: number;
|
|
113
|
+
/**
|
|
114
|
+
* Maximum retained change-journal entries after trimming. Defaults to 1024.
|
|
115
|
+
* A non-positive or non-finite value keeps the journal unbounded; a
|
|
116
|
+
* positive fraction retains at least one entry.
|
|
117
|
+
*/
|
|
118
|
+
readonly journalCapacity?: number;
|
|
119
|
+
}
|
|
120
|
+
/** A snapshot of what the replica currently materializes and retains. */
|
|
121
|
+
interface ReplicaStats {
|
|
122
|
+
/** Number of row objects currently materialized (tombstones included). */
|
|
123
|
+
readonly entities: number;
|
|
124
|
+
/** Materialized row count per entity type. */
|
|
125
|
+
readonly byType: Readonly<Record<string, number>>;
|
|
126
|
+
/** Rows retained because their type is held: the metadata footprint. */
|
|
127
|
+
readonly held: number;
|
|
128
|
+
/** Held row count per type; excludes opened on-demand rows. */
|
|
129
|
+
readonly heldByType: Readonly<Record<string, number>>;
|
|
130
|
+
/** Resident on-demand rows (open or closed and waiting out their TTL). */
|
|
131
|
+
readonly pinned: number;
|
|
132
|
+
readonly revision: Revision;
|
|
133
|
+
/** Append cursor of the last durable position folded into the base. */
|
|
134
|
+
readonly position: number;
|
|
135
|
+
/** Retained journal entries. */
|
|
136
|
+
readonly changes: number;
|
|
137
|
+
/** Oldest revision after which `changesSince` is complete. */
|
|
138
|
+
readonly changeFloor: Revision;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Adjacency read key for the replica's relationship index. `sourceId`
|
|
142
|
+
* reads forward — the targets out of the source; `targetId` reads
|
|
143
|
+
* reverse — the sources pointing at the target. `sourceId` wins when
|
|
144
|
+
* both are set, and neither yields no ids.
|
|
145
|
+
*/
|
|
146
|
+
interface RelationshipQuery {
|
|
147
|
+
readonly as: string;
|
|
148
|
+
readonly type: string;
|
|
149
|
+
readonly sourceId?: string;
|
|
150
|
+
readonly targetId?: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Adjacency read key for the replica's membership index. `groupId`
|
|
154
|
+
* reads the group's members; `entityId` reads the entity's groups.
|
|
155
|
+
* `groupId` wins when both are set, and neither yields no ids.
|
|
156
|
+
*/
|
|
157
|
+
interface MembershipQuery {
|
|
158
|
+
readonly groupId?: string;
|
|
159
|
+
readonly entityId?: string;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The materialized read path. Writes are synchronous and single-writer: the
|
|
163
|
+
* caller serializes them and folds durable updates in ascending log position.
|
|
164
|
+
* Reads return the replica's own row objects (shared, not cloned), tombstones
|
|
165
|
+
* included; callers must treat them as immutable.
|
|
166
|
+
*/
|
|
167
|
+
interface Replica {
|
|
168
|
+
readonly revision: Revision;
|
|
169
|
+
/**
|
|
170
|
+
* Smallest revision for which the retained journal is complete: the
|
|
171
|
+
* revision of the newest trimmed change, or the loaded record's revision.
|
|
172
|
+
* A `changesSince` below this floor yields nothing because everything may
|
|
173
|
+
* have changed.
|
|
174
|
+
*/
|
|
175
|
+
readonly changeFloor: Revision;
|
|
176
|
+
get(id: string): Entity | undefined;
|
|
177
|
+
/**
|
|
178
|
+
* The durable base row for `id`, without this client's optimistic overlay
|
|
179
|
+
* writes. A reader judging "has this row already folded a later Action than
|
|
180
|
+
* the one I am delivering" must read the base: an overlay `put` composes
|
|
181
|
+
* with gsn 0, which would hide the row's folded position.
|
|
182
|
+
*/
|
|
183
|
+
base(id: string): Entity | undefined;
|
|
184
|
+
iterate(type: string): readonly Entity[];
|
|
185
|
+
/** Whether the materialization policy holds `type` in full. */
|
|
186
|
+
isHeld(type: string): boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Load `id` through `readEntity` when absent, pinning it and journaling the
|
|
189
|
+
* install so a derived reader sees the row arrive. Idempotent: a resident row
|
|
190
|
+
* is marked open and touched (LRU order) without another read, a concurrent
|
|
191
|
+
* open shares the in-flight read, and a held row is returned without
|
|
192
|
+
* installing or pinning. Resolves to the composed row, or `null` when the
|
|
193
|
+
* read misses.
|
|
194
|
+
*/
|
|
195
|
+
open(id: string): Promise<Entity | null>;
|
|
196
|
+
/**
|
|
197
|
+
* Close a pinned row. It stays cached until `pinnedTtlMs` so a remount reads
|
|
198
|
+
* it back, then leaves; with the TTL disabled it leaves immediately. A no-op
|
|
199
|
+
* for held rows.
|
|
200
|
+
*/
|
|
201
|
+
close(id: string): void;
|
|
202
|
+
/** Whether `id` is currently open (closed rows waiting out their TTL are not). */
|
|
203
|
+
isOpen(id: string): boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Changes newer than `revision`, ascending. Empty when the cursor predates
|
|
206
|
+
* `changeFloor`, where the trimmed journal can no longer answer it.
|
|
207
|
+
*/
|
|
208
|
+
changesSince(revision: Revision): readonly ReplicaChange[];
|
|
209
|
+
stats(): ReplicaStats;
|
|
210
|
+
/**
|
|
211
|
+
* Distinct live targets out of `(as, type, sourceId)`, or distinct
|
|
212
|
+
* live sources pointing at `(as, type, targetId)`, ascending.
|
|
213
|
+
*/
|
|
214
|
+
relationships(query: RelationshipQuery): readonly string[];
|
|
215
|
+
/**
|
|
216
|
+
* Distinct live member entity ids of a group, or distinct live group
|
|
217
|
+
* ids of an entity, ascending.
|
|
218
|
+
*/
|
|
219
|
+
memberships(query: MembershipQuery): readonly string[];
|
|
220
|
+
/** Live forward relationship edges out of a source, ascending by target then row id. */
|
|
221
|
+
relationshipLinks(query: RelationshipSourceQuery): readonly RelationshipLink[];
|
|
222
|
+
/** Live member-keyed membership rows of an entity, ascending by group then row id. */
|
|
223
|
+
membershipLinks(entityId: string): readonly MembershipLink[];
|
|
224
|
+
/** Fold one durable Action's `delta` at its log position. */
|
|
225
|
+
ingest(action: Action, delta: readonly Update[], position: number): Revision;
|
|
226
|
+
/** Fold an unacknowledged local Action into the overlay only. */
|
|
227
|
+
optimistic(action: Action): Revision;
|
|
228
|
+
/** Drop an overlay Action's writes, reverting the composed view. */
|
|
229
|
+
discard(actionId: string): Revision;
|
|
230
|
+
/**
|
|
231
|
+
* Replace the materialized base with `record`. The journal resets to that
|
|
232
|
+
* baseline. The revision advances to the record's revision when that is
|
|
233
|
+
* newer, and by one otherwise, so a derived reader always observes the
|
|
234
|
+
* wholesale replacement even when the record is the same revision.
|
|
235
|
+
*/
|
|
236
|
+
load(record: ReplicaRecord): Revision;
|
|
237
|
+
toRecord(): ReplicaRecord;
|
|
238
|
+
/**
|
|
239
|
+
* Drop rows from the in-memory base and indices without tombstoning them:
|
|
240
|
+
* afterwards `get(id)` is undefined and `iterate(type)` omits the row. Each
|
|
241
|
+
* removed row journals one change, so derived readers see it leave. Absent
|
|
242
|
+
* ids are ignored. Returns the resulting revision.
|
|
243
|
+
*/
|
|
244
|
+
evict(ids: readonly string[]): Revision;
|
|
245
|
+
/**
|
|
246
|
+
* Subscribe to post-change notifications. Fires after every mutating call
|
|
247
|
+
* (`ingest` / `optimistic` / `discard` / `load` / `evict` / `open` /
|
|
248
|
+
* `close`) that settles the composed view, so a derived reader can
|
|
249
|
+
* resynchronise from `revision` and `changesSince`. A no-op write still
|
|
250
|
+
* fires; readers cheaply ignore an unchanged revision. Re-opening a resident
|
|
251
|
+
* row only touches the LRU window, so it fires nothing. Returns an
|
|
252
|
+
* unsubscribe function.
|
|
253
|
+
*/
|
|
254
|
+
subscribe(listener: () => void): () => void;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
declare const holdAll: MaterializationPolicy;
|
|
258
|
+
declare const DEFAULT_PINNED_CAPACITY = 16;
|
|
259
|
+
declare const DEFAULT_PINNED_TTL_MS = 30000;
|
|
260
|
+
/**
|
|
261
|
+
* Mutable in-place core. `entities` is the durable base (what `toRecord`
|
|
262
|
+
* persists, once pinned on-demand rows are filtered out), `overlay` composes
|
|
263
|
+
* optimistic writes over it, and `effective` caches the composed view for
|
|
264
|
+
* overlaid ids so a read never recomposes.
|
|
265
|
+
* `typeIndex` indexes the base; `iterate` layers the overlay's type overrides
|
|
266
|
+
* on top of it.
|
|
267
|
+
*/
|
|
268
|
+
declare const createReplica: (options?: ReplicaOptions) => Replica;
|
|
269
|
+
|
|
270
|
+
export { DEFAULT_PINNED_CAPACITY, DEFAULT_PINNED_TTL_MS, applyUpdate, contentChanged, createReplica, holdAll };
|
|
271
|
+
export type { MaterializationPolicy, MembershipQuery, RelationshipQuery, Replica, ReplicaChange, ReplicaOptions, ReplicaStats, Revision };
|