@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.
@@ -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 };