@ebbjs/storage 0.3.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/indexeddb/index.d.ts +55 -20
- package/dist/indexeddb/index.js +202 -179
- package/dist/memory/index.d.ts +56 -21
- package/dist/memory/index.js +105 -87
- package/dist/replica/index.d.ts +49 -11
- package/dist/replica/index.js +209 -200
- package/dist/replica-record-BaptWabD.js +243 -0
- package/dist/types/index.d.ts +92 -23
- package/package.json +2 -2
- package/dist/replica-record-D3lkFACh.js +0 -167
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { isFieldMap as u, mergeFieldValue as p, latestHlc as F, componentModel as m, componentField as H, isFieldLeaf as W, componentName as b } from "@ebbjs/core";
|
|
2
|
+
const B = 500, y = 64, h = /* @__PURE__ */ new WeakMap(), K = (e) => {
|
|
3
|
+
let t = 2166136261;
|
|
4
|
+
for (let n = 0; n < e.length; n += 1)
|
|
5
|
+
t ^= e.charCodeAt(n), t = Math.imul(t, 16777619) >>> 0;
|
|
6
|
+
return t >>> 0;
|
|
7
|
+
}, R = (e) => K(e) % y, V = () => Array.from({ length: y }, () => ({})), k = (e, t) => {
|
|
8
|
+
let n = t;
|
|
9
|
+
const o = {
|
|
10
|
+
get map() {
|
|
11
|
+
if (n === void 0) {
|
|
12
|
+
const r = {};
|
|
13
|
+
for (const s of e) Object.assign(r, s);
|
|
14
|
+
n = r;
|
|
15
|
+
}
|
|
16
|
+
return n;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
return h.set(o, e), o;
|
|
20
|
+
}, I = (e) => u(e) && h.has(e), Y = (e) => {
|
|
21
|
+
const t = h.get(e);
|
|
22
|
+
if (t === void 0) throw new Error("not a sharded map field");
|
|
23
|
+
return t;
|
|
24
|
+
}, $ = (e) => e.map, z = (e) => {
|
|
25
|
+
const t = V(), n = {};
|
|
26
|
+
for (const [o, r] of Object.entries(e))
|
|
27
|
+
t[R(o)][o] = r, n[o] = r;
|
|
28
|
+
return k(t, n);
|
|
29
|
+
}, O = (e) => !u(e) || I(e) ? e : Object.keys(e.map).length >= B ? z(e.map) : e, j = (e) => {
|
|
30
|
+
let t;
|
|
31
|
+
for (const [n, o] of Object.entries(e)) {
|
|
32
|
+
const r = O(o);
|
|
33
|
+
r !== o && (t === void 0 && (t = { ...e }), t[n] = r);
|
|
34
|
+
}
|
|
35
|
+
return t ?? e;
|
|
36
|
+
}, U = (e) => {
|
|
37
|
+
let t;
|
|
38
|
+
for (const [n, o] of Object.entries(e))
|
|
39
|
+
!u(o) || !I(o) || (t === void 0 && (t = { ...e }), t[n] = { map: $(o) });
|
|
40
|
+
return t ?? e;
|
|
41
|
+
}, q = (e, t) => {
|
|
42
|
+
const n = Y(e), o = [...n], r = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const [s, l] of Object.entries(t.map)) {
|
|
44
|
+
const a = R(s), v = (r.get(a) ?? n[a])[s], g = p(v, l);
|
|
45
|
+
if (g === v) continue;
|
|
46
|
+
let i = r.get(a);
|
|
47
|
+
i === void 0 && (i = { ...n[a] }, r.set(a, i)), i[s] = g;
|
|
48
|
+
}
|
|
49
|
+
if (r.size === 0) return e;
|
|
50
|
+
for (const [s, l] of r) o[s] = l;
|
|
51
|
+
return k(o);
|
|
52
|
+
}, G = (e, t) => {
|
|
53
|
+
if (e !== void 0 && u(e) && I(e))
|
|
54
|
+
return u(t) ? q(e, t) : t;
|
|
55
|
+
const n = p(e, t);
|
|
56
|
+
return n === e ? n : O(n);
|
|
57
|
+
}, he = (e, t) => e.method === "delete" ? t?.type ?? "" : m(e.component), J = (e) => {
|
|
58
|
+
if (e.method === "delete") return null;
|
|
59
|
+
const t = H(e.component);
|
|
60
|
+
return e.method === "drop" ? { field: t, method: "drop", value: null } : { field: t, method: e.method, value: e.data };
|
|
61
|
+
}, Ie = { residentMapFields: !0 }, Q = (e, t, n) => {
|
|
62
|
+
const o = n?.residentMapFields ? G : p, r = e.fields[t.field], s = o(r, t.value);
|
|
63
|
+
return s === r ? e : { fields: { ...e.fields, [t.field]: s } };
|
|
64
|
+
}, A = (e, t) => {
|
|
65
|
+
const n = t(e.data.fields);
|
|
66
|
+
return n === e.data.fields ? e : { ...e, data: { ...e.data, fields: n } };
|
|
67
|
+
}, _e = (e) => A(e, j), we = (e) => A(e, U), X = (e, t, n, o, r) => {
|
|
68
|
+
const s = { [t.field]: t.value }, l = t.method === "replace";
|
|
69
|
+
return {
|
|
70
|
+
id: e.subject_id,
|
|
71
|
+
type: m(e.component),
|
|
72
|
+
data: { fields: r?.residentMapFields ? j(s) : s },
|
|
73
|
+
// A `replace` is the row's create; a `merge` arrives first only when the
|
|
74
|
+
// replica has not seen that create, so the placeholder waits for the base.
|
|
75
|
+
exists: l,
|
|
76
|
+
created_hlc: l ? o : null,
|
|
77
|
+
updated_hlc: o,
|
|
78
|
+
deleted_hlc: null,
|
|
79
|
+
last_gsn: n
|
|
80
|
+
};
|
|
81
|
+
}, Z = (e, t, n, o, r) => {
|
|
82
|
+
if (t.method === "delete")
|
|
83
|
+
return e ? {
|
|
84
|
+
...e,
|
|
85
|
+
deleted_hlc: o,
|
|
86
|
+
updated_hlc: o,
|
|
87
|
+
last_gsn: Math.max(e.last_gsn, n)
|
|
88
|
+
} : null;
|
|
89
|
+
const s = J(t);
|
|
90
|
+
if (s === null) return e;
|
|
91
|
+
if (s.method === "drop") {
|
|
92
|
+
if (!e) return null;
|
|
93
|
+
const { [s.field]: a, ...w } = e.data.fields;
|
|
94
|
+
return {
|
|
95
|
+
...e,
|
|
96
|
+
data: { fields: w },
|
|
97
|
+
updated_hlc: F(e.updated_hlc, o),
|
|
98
|
+
last_gsn: Math.max(e.last_gsn, n)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!e) return X(t, s, n, o, r);
|
|
102
|
+
const l = s.method === "replace" && !e.exists;
|
|
103
|
+
return {
|
|
104
|
+
...e,
|
|
105
|
+
type: m(t.component),
|
|
106
|
+
// A leaf `replace` is the leaf-level LWW compare, so it shares the merge
|
|
107
|
+
// rule: an out-of-order base snapshot must not clobber a newer write.
|
|
108
|
+
data: Q(e.data, s, r),
|
|
109
|
+
exists: e.exists || s.method === "replace",
|
|
110
|
+
created_hlc: l ? o : e.created_hlc,
|
|
111
|
+
updated_hlc: F(e.updated_hlc, o),
|
|
112
|
+
deleted_hlc: null,
|
|
113
|
+
last_gsn: Math.max(e.last_gsn, n)
|
|
114
|
+
};
|
|
115
|
+
}, ee = (e, t, n) => {
|
|
116
|
+
let o = e;
|
|
117
|
+
for (const r of n.updates)
|
|
118
|
+
r.subject_id === t && (o = Z(o, r, n.gsn, n.hlc));
|
|
119
|
+
return o;
|
|
120
|
+
}, ve = (e, t) => {
|
|
121
|
+
let n = null;
|
|
122
|
+
for (const o of e)
|
|
123
|
+
n = ee(n, t, o);
|
|
124
|
+
return n;
|
|
125
|
+
}, T = "relationship", f = "entityGroup", C = "groups", _ = (e, t, n) => `${e}\0${t}\0${n}`, D = (e, t, n) => `${_(e, t, n)}`, te = (e) => _(C, f, e), ne = (e) => D(C, f, e), d = (e, t) => {
|
|
126
|
+
const n = e.data?.fields?.[t];
|
|
127
|
+
if (n === void 0 || !W(n)) return null;
|
|
128
|
+
const o = n.value;
|
|
129
|
+
return typeof o == "string" ? o : null;
|
|
130
|
+
}, oe = (e) => {
|
|
131
|
+
if (e.type !== T || e.deleted_hlc !== null) return null;
|
|
132
|
+
const t = d(e, "field"), n = d(e, "type"), o = d(e, "target_id"), r = d(e, "source_id");
|
|
133
|
+
return t === null || n === null || o === null || r === null ? null : { key: _(t, n, o), rowId: e.id, value: r };
|
|
134
|
+
}, re = (e) => {
|
|
135
|
+
if (e.type !== T || e.deleted_hlc !== null) return null;
|
|
136
|
+
const t = d(e, "field"), n = d(e, "type"), o = d(e, "source_id"), r = d(e, "target_id");
|
|
137
|
+
return t === null || n === null || o === null || r === null ? null : { key: D(t, n, o), rowId: e.id, value: r };
|
|
138
|
+
}, se = (e) => {
|
|
139
|
+
if (e.type !== f || e.deleted_hlc !== null) return null;
|
|
140
|
+
const t = d(e, "group_id"), n = d(e, "entity_id");
|
|
141
|
+
return t === null || n === null ? null : { key: te(t), rowId: e.id, value: n };
|
|
142
|
+
}, le = (e) => {
|
|
143
|
+
if (e.type !== f || e.deleted_hlc !== null) return null;
|
|
144
|
+
const t = d(e, "group_id"), n = d(e, "entity_id");
|
|
145
|
+
return t === null || n === null ? null : { key: ne(n), rowId: e.id, value: t };
|
|
146
|
+
}, de = (e) => oe(e) ?? se(e), ce = (e) => re(e) ?? le(e), x = (e) => [...e].sort(), ge = (e) => x(new Set(Object.values(e ?? {}))), P = (e) => Object.entries(e ?? {}).map(([t, n]) => ({ rowId: t, value: n })).sort(
|
|
147
|
+
(t, n) => t.value === n.value ? t.rowId.localeCompare(n.rowId) : t.value.localeCompare(n.value)
|
|
148
|
+
), Fe = (e) => P(e).map(({ rowId: t, value: n }) => ({ relationshipId: t, targetId: n })), Me = (e) => P(e).map(({ rowId: t, value: n }) => ({ membershipId: t, groupId: n })), N = (e, t, n) => {
|
|
149
|
+
const o = { ...e };
|
|
150
|
+
return n ? o[t.rowId] = t.value : delete o[t.rowId], o;
|
|
151
|
+
}, ae = (e, t) => N(e, t, !0), ue = (e, t) => {
|
|
152
|
+
const n = N(e, t, !1);
|
|
153
|
+
return Object.keys(n).length === 0 ? null : n;
|
|
154
|
+
}, ie = (e, t) => e?.key === t?.key && e?.rowId === t?.rowId && e?.value === t?.value, L = (e) => (t, n) => {
|
|
155
|
+
const o = t === void 0 ? null : e(t), r = n === void 0 ? null : e(n);
|
|
156
|
+
return ie(o, r) ? { remove: null, add: null } : { remove: o, add: r };
|
|
157
|
+
}, Ee = L(de), Se = L(ce), M = (e, t, n) => {
|
|
158
|
+
if (n === null) {
|
|
159
|
+
const o = { ...e };
|
|
160
|
+
return delete o[t.key], o;
|
|
161
|
+
}
|
|
162
|
+
return { ...e, [t.key]: n };
|
|
163
|
+
}, be = (e, t) => {
|
|
164
|
+
if (t.remove === null && t.add === null) return e;
|
|
165
|
+
let n = e;
|
|
166
|
+
return t.remove !== null && (n = M(n, t.remove, ue(n[t.remove.key], t.remove))), t.add !== null && (n = M(n, t.add, ae(n[t.add.key], t.add))), n;
|
|
167
|
+
}, c = (e) => Object.keys(e.data?.fields ?? {}).map((t) => b(e.type, t)), E = (e) => e.deleted_hlc !== null, ye = (e, t) => {
|
|
168
|
+
if (e === t) return [];
|
|
169
|
+
if (e == null)
|
|
170
|
+
return t == null ? [] : c(t);
|
|
171
|
+
if (t == null) return c(e);
|
|
172
|
+
if (e.type !== t.type || E(e) !== E(t))
|
|
173
|
+
return [.../* @__PURE__ */ new Set([...c(e), ...c(t)])];
|
|
174
|
+
const n = e.data?.fields ?? {}, o = t.data?.fields ?? {}, r = [], s = /* @__PURE__ */ new Set([...Object.keys(n), ...Object.keys(o)]);
|
|
175
|
+
for (const l of s)
|
|
176
|
+
n[l] !== o[l] && r.push(b(t.type, l));
|
|
177
|
+
return r;
|
|
178
|
+
}, Re = (e, t) => {
|
|
179
|
+
if (e === t || e === void 0 && t === void 0) return null;
|
|
180
|
+
const n = e === void 0 ? [] : c(e), o = t === void 0 ? [] : c(t), r = new Set(n), s = new Set(o);
|
|
181
|
+
return {
|
|
182
|
+
rowId: (t ?? e).id,
|
|
183
|
+
remove: n.filter((l) => !s.has(l)),
|
|
184
|
+
add: o.filter((l) => !r.has(l))
|
|
185
|
+
};
|
|
186
|
+
}, ke = (e, t) => {
|
|
187
|
+
if (t.add.length === 0 && t.remove.length === 0) return e;
|
|
188
|
+
const n = new Map(e);
|
|
189
|
+
for (const o of t.remove) {
|
|
190
|
+
const r = n.get(o);
|
|
191
|
+
if (r === void 0) continue;
|
|
192
|
+
const s = new Set(r);
|
|
193
|
+
s.delete(t.rowId), s.size === 0 ? n.delete(o) : n.set(o, s);
|
|
194
|
+
}
|
|
195
|
+
for (const o of t.add)
|
|
196
|
+
n.set(o, /* @__PURE__ */ new Set([...n.get(o) ?? [], t.rowId]));
|
|
197
|
+
return n;
|
|
198
|
+
}, Oe = (e) => {
|
|
199
|
+
const t = /* @__PURE__ */ new Map();
|
|
200
|
+
for (const n of e)
|
|
201
|
+
for (const o of c(n)) {
|
|
202
|
+
const r = t.get(o);
|
|
203
|
+
r === void 0 ? t.set(o, /* @__PURE__ */ new Set([n.id])) : r.add(n.id);
|
|
204
|
+
}
|
|
205
|
+
return t;
|
|
206
|
+
}, je = (e, t) => x(e.get(t) ?? []), fe = 2, S = (e) => typeof e == "number" && Number.isInteger(e) && e >= 0, pe = (e) => typeof e == "object" && e !== null && typeof e.id == "string" && typeof e.type == "string" && typeof e.exists == "boolean" && (e.created_hlc === null || typeof e.created_hlc == "string"), Ae = (e) => {
|
|
207
|
+
if (typeof e != "object" || e === null) return !1;
|
|
208
|
+
const t = e;
|
|
209
|
+
return t.format !== fe || !S(t.position) || !S(t.revision) || !Array.isArray(t.entities) ? !1 : t.entities.every(pe);
|
|
210
|
+
};
|
|
211
|
+
export {
|
|
212
|
+
T as A,
|
|
213
|
+
ue as B,
|
|
214
|
+
ae as C,
|
|
215
|
+
f as M,
|
|
216
|
+
fe as R,
|
|
217
|
+
Fe as a,
|
|
218
|
+
ge as b,
|
|
219
|
+
ne as c,
|
|
220
|
+
je as d,
|
|
221
|
+
c as e,
|
|
222
|
+
D as f,
|
|
223
|
+
_e as g,
|
|
224
|
+
de as h,
|
|
225
|
+
Ae as i,
|
|
226
|
+
ce as j,
|
|
227
|
+
Oe as k,
|
|
228
|
+
Me as l,
|
|
229
|
+
te as m,
|
|
230
|
+
be as n,
|
|
231
|
+
Ee as o,
|
|
232
|
+
Se as p,
|
|
233
|
+
Re as q,
|
|
234
|
+
_ as r,
|
|
235
|
+
ke as s,
|
|
236
|
+
Z as t,
|
|
237
|
+
he as u,
|
|
238
|
+
ye as v,
|
|
239
|
+
we as w,
|
|
240
|
+
Ie as x,
|
|
241
|
+
ee as y,
|
|
242
|
+
ve as z
|
|
243
|
+
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -122,9 +122,10 @@ interface MembershipLink {
|
|
|
122
122
|
* handled by the DirtyTracker.
|
|
123
123
|
*
|
|
124
124
|
* ## Merge Semantics
|
|
125
|
-
* - **
|
|
126
|
-
* - **
|
|
127
|
-
* - **
|
|
125
|
+
* - **merge** — Last-Writer-Wins (LWW) at the component's field: higher HLC wins; tiebreak by lexicographic `update_id`
|
|
126
|
+
* - **replace** — the component's field is set whole; the first write to an absent row creates it
|
|
127
|
+
* - **drop** — the component's field is removed; the row survives
|
|
128
|
+
* - **delete** — soft delete (sets `deleted_hlc`); the row keeps its type
|
|
128
129
|
*
|
|
129
130
|
* A field value is either a leaf merged by the LWW rule above, or a
|
|
130
131
|
* `{ map }` of nested field values merged key by key under the same
|
|
@@ -136,6 +137,9 @@ interface MembershipLink {
|
|
|
136
137
|
* - `get(id)` — returns entity by ID, materializes if dirty
|
|
137
138
|
* - `set(entity)` — directly sets an entity (bypasses materialization)
|
|
138
139
|
* - `query(type)` — returns all materialized entities of a type
|
|
140
|
+
* - `queryByComponent(component)` — returns the live ids carrying that
|
|
141
|
+
* `<model>/<field>` component in ascending order, materializing dirty rows
|
|
142
|
+
* of the component's model first
|
|
139
143
|
* - `queryByRelationship({ as, type, targetId })` — returns the distinct
|
|
140
144
|
* source ids indexed under that triple in ascending order,
|
|
141
145
|
* materializing dirty relationship rows first
|
|
@@ -154,6 +158,18 @@ interface EntityStore {
|
|
|
154
158
|
get(id: string): Promise<Entity | null>;
|
|
155
159
|
set(entity: Entity): Promise<void>;
|
|
156
160
|
query(type: string): Promise<readonly Entity[]>;
|
|
161
|
+
/**
|
|
162
|
+
* Live rows currently carrying `component` (`<model>/<field>`), ascending by
|
|
163
|
+
* id. Existence is the set of present fields on a row, so this is an index
|
|
164
|
+
* read rather than a row filter; a tombstoned row's components are not
|
|
165
|
+
* existence.
|
|
166
|
+
*
|
|
167
|
+
* The dirty rows materialized first are the component prefix's model. That is
|
|
168
|
+
* the current `<model>/<field>` addressing rule (#797); a component namespace
|
|
169
|
+
* that is not the row's model (#805's `ebb/*` components on ordinary rows)
|
|
170
|
+
* has to decode the model differently and revisit this step.
|
|
171
|
+
*/
|
|
172
|
+
queryByComponent(component: string): Promise<readonly string[]>;
|
|
157
173
|
queryByRelationship(query: RelationshipIndexQuery): Promise<readonly string[]>;
|
|
158
174
|
queryByRelationshipSource(query: RelationshipSourceQuery): Promise<readonly RelationshipLink[]>;
|
|
159
175
|
queryByMembership(groupId: string): Promise<readonly string[]>;
|
|
@@ -263,8 +279,15 @@ type Revision = number;
|
|
|
263
279
|
interface ReplicaChange {
|
|
264
280
|
readonly revision: Revision;
|
|
265
281
|
readonly id: string;
|
|
266
|
-
readonly type: string;
|
|
267
282
|
readonly deleted: boolean;
|
|
283
|
+
/**
|
|
284
|
+
* The components this change touched, so a derived reader invalidates on what
|
|
285
|
+
* it read rather than on the row's whole model. A tombstone transition touches
|
|
286
|
+
* every component the row carries, and a write that moves a row between models
|
|
287
|
+
* names each field under both prefixes. Empty only for a row that carried no
|
|
288
|
+
* component.
|
|
289
|
+
*/
|
|
290
|
+
readonly components: readonly string[];
|
|
268
291
|
}
|
|
269
292
|
/**
|
|
270
293
|
* Whether the replica retains a type's rows. The seam #500 builds on-demand
|
|
@@ -364,7 +387,11 @@ interface Replica {
|
|
|
364
387
|
* with gsn 0, which would hide the row's folded position.
|
|
365
388
|
*/
|
|
366
389
|
base(id: string): Entity | undefined;
|
|
367
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Every row of `model`, whatever optional components it carries. Served by
|
|
392
|
+
* the model index with the overlay's model overrides layered on top.
|
|
393
|
+
*/
|
|
394
|
+
iterate(model: string): readonly Entity[];
|
|
368
395
|
/** Whether the materialization policy holds `type` in full. */
|
|
369
396
|
isHeld(type: string): boolean;
|
|
370
397
|
/**
|
|
@@ -395,6 +422,12 @@ interface Replica {
|
|
|
395
422
|
* live sources pointing at `(as, type, targetId)`, ascending.
|
|
396
423
|
*/
|
|
397
424
|
relationships(query: RelationshipQuery): readonly string[];
|
|
425
|
+
/**
|
|
426
|
+
* Ids currently carrying `component` (`<type>/<field>`) in the composed
|
|
427
|
+
* view, ascending. Component existence is the set of present fields on a
|
|
428
|
+
* row, so a query reading it costs its index rather than a row filter.
|
|
429
|
+
*/
|
|
430
|
+
components(component: string): readonly string[];
|
|
398
431
|
/**
|
|
399
432
|
* Distinct live member entity ids of a group, or distinct live group
|
|
400
433
|
* ids of an entity, ascending.
|
|
@@ -445,44 +478,51 @@ interface Replica {
|
|
|
445
478
|
* `conflicted` is a terminal-until-resolved hold: a catch-up pass
|
|
446
479
|
* classified the Action as a loser, so it is structurally excluded from
|
|
447
480
|
* `pending()` and never submitted. The application resolves it by
|
|
448
|
-
*
|
|
481
|
+
* sending the Action unchanged (back to `pending`) or clearing it
|
|
482
|
+
* (removed).
|
|
449
483
|
*/
|
|
450
484
|
type OutboxStatus = "pending" | "acknowledged" | "error" | "conflicted";
|
|
451
485
|
/**
|
|
452
486
|
* One side of a contested slot: the buffered local write, or the value
|
|
453
487
|
* the server holds once the catch-up window is applied. A leaf carries
|
|
454
|
-
* its LWW triple; a whole-
|
|
488
|
+
* its LWW triple; a whole-component map replacement has no single leaf, so
|
|
455
489
|
* `update_id` / `hlc` are absent and `value` is the projected map. A
|
|
456
|
-
*
|
|
490
|
+
* component an inbound `merge` or `delete` reverted also has no triple, and
|
|
457
491
|
* `value` is `undefined` because the server holds no value.
|
|
458
492
|
*/
|
|
459
493
|
interface ConflictSide {
|
|
460
494
|
/** Projected leaf or map value. `undefined` when the server holds nothing. */
|
|
461
495
|
readonly value: unknown;
|
|
462
|
-
/** `update_id` of the FieldValue; absent for a map side or a reverted
|
|
496
|
+
/** `update_id` of the FieldValue; absent for a map side or a reverted component. */
|
|
463
497
|
readonly update_id?: string;
|
|
464
|
-
/** HLC carried by the FieldValue; absent for a map side or a reverted
|
|
498
|
+
/** HLC carried by the FieldValue; absent for a map side or a reverted component. */
|
|
465
499
|
readonly hlc?: string;
|
|
466
500
|
}
|
|
467
501
|
/**
|
|
468
|
-
* One contested position on a losing Action: a
|
|
469
|
-
* path from the
|
|
470
|
-
* leaf
|
|
471
|
-
*
|
|
502
|
+
* One contested position on a losing Action: a component plus the map-key
|
|
503
|
+
* path from the component's value root to the position. `path` is empty for a
|
|
504
|
+
* plain leaf component, an entity-level touch that overlaps no position, or a
|
|
505
|
+
* component an inbound `merge` / `delete` reverted. Concurrent writes to
|
|
472
506
|
* different keys are different slots and so are not conflicts.
|
|
473
507
|
*/
|
|
474
508
|
interface ConflictSlot {
|
|
475
509
|
/** The `subject_id` of the Update that carried the write. */
|
|
476
510
|
readonly subjectId: string;
|
|
477
|
-
/** The field name on that subject. */
|
|
478
|
-
readonly
|
|
479
|
-
/** Ordered map keys from the
|
|
511
|
+
/** The `<model>/<field>` component name on that subject. */
|
|
512
|
+
readonly component: string;
|
|
513
|
+
/** Ordered map keys from the component root to the position; empty at the root. */
|
|
480
514
|
readonly path: readonly string[];
|
|
481
515
|
/** The buffered local value at the position. */
|
|
482
516
|
readonly local: ConflictSide;
|
|
483
517
|
/** The value the server holds at the position after the missed window. */
|
|
484
518
|
readonly remote: ConflictSide;
|
|
485
519
|
}
|
|
520
|
+
/** One missed Action that out-dated a buffered write and made it a conflict. */
|
|
521
|
+
interface ConflictRemoteAction {
|
|
522
|
+
readonly actorId: string;
|
|
523
|
+
readonly actionId: string;
|
|
524
|
+
readonly gsn: number;
|
|
525
|
+
}
|
|
486
526
|
/**
|
|
487
527
|
* The before/after entity rows for one losing subject. `before` is the
|
|
488
528
|
* row as the client had it immediately before the catch-up window was
|
|
@@ -494,7 +534,8 @@ interface ConflictSlot {
|
|
|
494
534
|
*/
|
|
495
535
|
interface ConflictSubjectState {
|
|
496
536
|
readonly subjectId: string;
|
|
497
|
-
|
|
537
|
+
/** The model the row is committed under. */
|
|
538
|
+
readonly model: string;
|
|
498
539
|
readonly before: Entity | null;
|
|
499
540
|
readonly after: Entity | null;
|
|
500
541
|
}
|
|
@@ -521,6 +562,18 @@ interface OutboxConflict {
|
|
|
521
562
|
* application reads that as "nothing of mine would be lost".
|
|
522
563
|
*/
|
|
523
564
|
readonly slots: readonly ConflictSlot[];
|
|
565
|
+
/**
|
|
566
|
+
* The looser of the losing subjects' declared scopes: `"entity"` when
|
|
567
|
+
* any losing subject declared entity scope, else `"field"`. The union is
|
|
568
|
+
* spelled inline so storage stays independent of the schema package.
|
|
569
|
+
*/
|
|
570
|
+
readonly scope: "field" | "component" | "entity";
|
|
571
|
+
/**
|
|
572
|
+
* The concurrent missed Actions that touched the losing subjects,
|
|
573
|
+
* deduped by action id, in window order. The remote half of the
|
|
574
|
+
* report an application builds a policy from.
|
|
575
|
+
*/
|
|
576
|
+
readonly remoteActions: readonly ConflictRemoteAction[];
|
|
524
577
|
/** The catch-up pass that produced this entry. */
|
|
525
578
|
readonly catchUpId: string;
|
|
526
579
|
/**
|
|
@@ -532,6 +585,16 @@ interface OutboxConflict {
|
|
|
532
585
|
/** Client HLC stamped at detection. The conflict view's ordering key. */
|
|
533
586
|
readonly detectedAtHlc: string;
|
|
534
587
|
}
|
|
588
|
+
/**
|
|
589
|
+
* The conflict payload as a row written before IndexedDB schema v10
|
|
590
|
+
* persisted it: `scope` and `remoteActions` were added at v10. The store's
|
|
591
|
+
* read path fills both in; a legacy row is still a field-scope conflict
|
|
592
|
+
* with no recorded remote context.
|
|
593
|
+
*/
|
|
594
|
+
interface StoredOutboxConflict extends Omit<OutboxConflict, "scope" | "remoteActions"> {
|
|
595
|
+
readonly scope?: "field" | "component" | "entity";
|
|
596
|
+
readonly remoteActions?: readonly ConflictRemoteAction[];
|
|
597
|
+
}
|
|
535
598
|
/**
|
|
536
599
|
* One locally-authored Action awaiting acknowledgement, plus the HLC at
|
|
537
600
|
* which it entered the outbox. A `conflicted` entry carries its
|
|
@@ -557,6 +620,13 @@ interface OutboxEntry {
|
|
|
557
620
|
/** Present iff `status === "conflicted"`. */
|
|
558
621
|
readonly conflict?: OutboxConflict;
|
|
559
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* The persisted form of an outbox entry. Identical to {@link OutboxEntry}
|
|
625
|
+
* except that a conflict payload may predate the v10 fields.
|
|
626
|
+
*/
|
|
627
|
+
interface StoredOutboxEntry extends Omit<OutboxEntry, "conflict"> {
|
|
628
|
+
readonly conflict?: StoredOutboxConflict;
|
|
629
|
+
}
|
|
560
630
|
/**
|
|
561
631
|
* OutboxStore — durable buffer of pending local Actions.
|
|
562
632
|
*
|
|
@@ -569,10 +639,9 @@ interface OutboxEntry {
|
|
|
569
639
|
interface OutboxStore {
|
|
570
640
|
put(entry: OutboxEntry): Promise<void>;
|
|
571
641
|
/**
|
|
572
|
-
* Persist `entry` and delete `deleteId` in one durable operation
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
* without the re-assert persisted.
|
|
642
|
+
* Persist `entry` and delete `deleteId` in one durable operation: the
|
|
643
|
+
* replacement must never exist without the replaced row being gone,
|
|
644
|
+
* nor the row be gone without the replacement persisted.
|
|
576
645
|
*/
|
|
577
646
|
putAndDelete(entry: OutboxEntry, deleteId: string): Promise<void>;
|
|
578
647
|
list(): Promise<readonly OutboxEntry[]>;
|
|
@@ -618,4 +687,4 @@ interface StorageAdapter {
|
|
|
618
687
|
reset(): Promise<void>;
|
|
619
688
|
}
|
|
620
689
|
|
|
621
|
-
export type { ActionLog, ConflictSide, ConflictSlot, ConflictSubjectState, CursorRecord, CursorStore, DirtyTracker, EntityStore, LoggedAction, MaterializationPolicy, MembershipLink, MembershipQuery, OutboxConflict, OutboxEntry, OutboxStatus, OutboxStore, RelationshipIndexQuery, RelationshipLink, RelationshipQuery, RelationshipSourceQuery, Replica, ReplicaChange, ReplicaOptions, ReplicaRecord, ReplicaStats, ReplicaStore, Revision, SeedMarker, StorageAdapter };
|
|
690
|
+
export type { ActionLog, ConflictRemoteAction, ConflictSide, ConflictSlot, ConflictSubjectState, CursorRecord, CursorStore, DirtyTracker, EntityStore, LoggedAction, MaterializationPolicy, MembershipLink, MembershipQuery, OutboxConflict, OutboxEntry, OutboxStatus, OutboxStore, RelationshipIndexQuery, RelationshipLink, RelationshipQuery, RelationshipSourceQuery, Replica, ReplicaChange, ReplicaOptions, ReplicaRecord, ReplicaStats, ReplicaStore, Revision, SeedMarker, StorageAdapter, StoredOutboxConflict, StoredOutboxEntry };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ebbjs/storage",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Storage components for ebbjs",
|
|
6
6
|
"homepage": "https://ebbjs.com",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@ebbjs/core": "^
|
|
47
|
+
"@ebbjs/core": "^1.1.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"fake-indexeddb": "^6.2.5",
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { isFieldMap as u, mergeFieldValue as m, latestHlc as P, isFieldLeaf as D } from "@ebbjs/core";
|
|
2
|
-
const L = 500, M = 64, I = /* @__PURE__ */ new WeakMap(), N = (e) => {
|
|
3
|
-
let r = 2166136261;
|
|
4
|
-
for (let t = 0; t < e.length; t += 1)
|
|
5
|
-
r ^= e.charCodeAt(t), r = Math.imul(r, 16777619) >>> 0;
|
|
6
|
-
return r >>> 0;
|
|
7
|
-
}, E = (e) => N(e) % M, C = () => Array.from({ length: M }, () => ({})), R = (e, r) => {
|
|
8
|
-
let t = r;
|
|
9
|
-
const n = {
|
|
10
|
-
get map() {
|
|
11
|
-
if (t === void 0) {
|
|
12
|
-
const s = {};
|
|
13
|
-
for (const o of e) Object.assign(s, o);
|
|
14
|
-
t = s;
|
|
15
|
-
}
|
|
16
|
-
return t;
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
return I.set(n, e), n;
|
|
20
|
-
}, _ = (e) => u(e) && I.has(e), H = (e) => {
|
|
21
|
-
const r = I.get(e);
|
|
22
|
-
if (r === void 0) throw new Error("not a sharded map field");
|
|
23
|
-
return r;
|
|
24
|
-
}, V = (e) => e.map, W = (e) => {
|
|
25
|
-
const r = C(), t = {};
|
|
26
|
-
for (const [n, s] of Object.entries(e))
|
|
27
|
-
r[E(n)][n] = s, t[n] = s;
|
|
28
|
-
return R(r, t);
|
|
29
|
-
}, b = (e) => !u(e) || _(e) ? e : Object.keys(e.map).length >= L ? W(e.map) : e, k = (e) => {
|
|
30
|
-
let r;
|
|
31
|
-
for (const [t, n] of Object.entries(e)) {
|
|
32
|
-
const s = b(n);
|
|
33
|
-
s !== n && (r === void 0 && (r = { ...e }), r[t] = s);
|
|
34
|
-
}
|
|
35
|
-
return r ?? e;
|
|
36
|
-
}, K = (e) => {
|
|
37
|
-
let r;
|
|
38
|
-
for (const [t, n] of Object.entries(e))
|
|
39
|
-
!u(n) || !_(n) || (r === void 0 && (r = { ...e }), r[t] = { map: V(n) });
|
|
40
|
-
return r ?? e;
|
|
41
|
-
}, Y = (e, r) => {
|
|
42
|
-
const t = H(e), n = [...t], s = /* @__PURE__ */ new Map();
|
|
43
|
-
for (const [o, d] of Object.entries(r.map)) {
|
|
44
|
-
const a = E(o), c = (s.get(a) ?? t[a])[o], F = m(c, d);
|
|
45
|
-
if (F === c) continue;
|
|
46
|
-
let i = s.get(a);
|
|
47
|
-
i === void 0 && (i = { ...t[a] }, s.set(a, i)), i[o] = F;
|
|
48
|
-
}
|
|
49
|
-
if (s.size === 0) return e;
|
|
50
|
-
for (const [o, d] of s) n[o] = d;
|
|
51
|
-
return R(n);
|
|
52
|
-
}, $ = (e, r) => {
|
|
53
|
-
if (e !== void 0 && u(e) && _(e))
|
|
54
|
-
return u(r) ? Y(e, r) : r;
|
|
55
|
-
const t = m(e, r);
|
|
56
|
-
return t === e ? t : b(t);
|
|
57
|
-
}, h = (e) => e.data?.fields ?? {}, de = { residentMapFields: !0 }, B = (e, r, t) => {
|
|
58
|
-
const n = h(r), s = t?.residentMapFields ? $ : m;
|
|
59
|
-
let o;
|
|
60
|
-
for (const [d, a] of Object.entries(n)) {
|
|
61
|
-
const p = o === void 0 ? e.fields[d] : o[d], c = s(p, a);
|
|
62
|
-
c !== p && (o === void 0 && (o = { ...e.fields }), o[d] = c);
|
|
63
|
-
}
|
|
64
|
-
return o === void 0 ? e : { fields: o };
|
|
65
|
-
}, y = (e, r) => {
|
|
66
|
-
const t = r(e.data.fields);
|
|
67
|
-
return t === e.data.fields ? e : { ...e, data: { ...e.data, fields: t } };
|
|
68
|
-
}, ae = (e) => y(e, k), ce = (e) => y(e, K), ue = (e, r, t, n, s) => {
|
|
69
|
-
switch (r.method) {
|
|
70
|
-
case "put":
|
|
71
|
-
return {
|
|
72
|
-
id: r.subject_id,
|
|
73
|
-
type: r.subject_type,
|
|
74
|
-
data: {
|
|
75
|
-
fields: s?.residentMapFields ? k(h(r)) : h(r)
|
|
76
|
-
},
|
|
77
|
-
created_hlc: n,
|
|
78
|
-
updated_hlc: n,
|
|
79
|
-
deleted_hlc: null,
|
|
80
|
-
last_gsn: t
|
|
81
|
-
};
|
|
82
|
-
case "patch":
|
|
83
|
-
return e ? e.deleted_hlc ? e : {
|
|
84
|
-
...e,
|
|
85
|
-
data: B(e.data, r, s),
|
|
86
|
-
updated_hlc: P(e.updated_hlc, n),
|
|
87
|
-
last_gsn: Math.max(e.last_gsn, t)
|
|
88
|
-
} : null;
|
|
89
|
-
case "delete":
|
|
90
|
-
return e ? {
|
|
91
|
-
...e,
|
|
92
|
-
deleted_hlc: n,
|
|
93
|
-
updated_hlc: n,
|
|
94
|
-
last_gsn: Math.max(e.last_gsn, t)
|
|
95
|
-
} : null;
|
|
96
|
-
}
|
|
97
|
-
}, S = "relationship", f = "entityGroup", O = "groups", v = (e, r, t) => `${e}\0${r}\0${t}`, j = (e, r, t) => `${v(e, r, t)}`, U = (e) => v(O, f, e), q = (e) => j(O, f, e), l = (e, r) => {
|
|
98
|
-
const t = e.data?.fields?.[r];
|
|
99
|
-
if (t === void 0 || !D(t)) return null;
|
|
100
|
-
const n = t.value;
|
|
101
|
-
return typeof n == "string" ? n : null;
|
|
102
|
-
}, z = (e) => {
|
|
103
|
-
if (e.type !== S || e.deleted_hlc !== null) return null;
|
|
104
|
-
const r = l(e, "field"), t = l(e, "type"), n = l(e, "target_id"), s = l(e, "source_id");
|
|
105
|
-
return r === null || t === null || n === null || s === null ? null : { key: v(r, t, n), rowId: e.id, value: s };
|
|
106
|
-
}, G = (e) => {
|
|
107
|
-
if (e.type !== S || e.deleted_hlc !== null) return null;
|
|
108
|
-
const r = l(e, "field"), t = l(e, "type"), n = l(e, "source_id"), s = l(e, "target_id");
|
|
109
|
-
return r === null || t === null || n === null || s === null ? null : { key: j(r, t, n), rowId: e.id, value: s };
|
|
110
|
-
}, J = (e) => {
|
|
111
|
-
if (e.type !== f || e.deleted_hlc !== null) return null;
|
|
112
|
-
const r = l(e, "group_id"), t = l(e, "entity_id");
|
|
113
|
-
return r === null || t === null ? null : { key: U(r), rowId: e.id, value: t };
|
|
114
|
-
}, Q = (e) => {
|
|
115
|
-
if (e.type !== f || e.deleted_hlc !== null) return null;
|
|
116
|
-
const r = l(e, "group_id"), t = l(e, "entity_id");
|
|
117
|
-
return r === null || t === null ? null : { key: q(t), rowId: e.id, value: r };
|
|
118
|
-
}, X = (e) => z(e) ?? J(e), Z = (e) => G(e) ?? Q(e), ee = (e) => [...e].sort(), ie = (e) => ee(new Set(Object.values(e ?? {}))), x = (e) => Object.entries(e ?? {}).map(([r, t]) => ({ rowId: r, value: t })).sort(
|
|
119
|
-
(r, t) => r.value === t.value ? r.rowId.localeCompare(t.rowId) : r.value.localeCompare(t.value)
|
|
120
|
-
), fe = (e) => x(e).map(({ rowId: r, value: t }) => ({ relationshipId: r, targetId: t })), pe = (e) => x(e).map(({ rowId: r, value: t }) => ({ membershipId: r, groupId: t })), A = (e, r, t) => {
|
|
121
|
-
const n = { ...e };
|
|
122
|
-
return t ? n[r.rowId] = r.value : delete n[r.rowId], n;
|
|
123
|
-
}, re = (e, r) => A(e, r, !0), te = (e, r) => {
|
|
124
|
-
const t = A(e, r, !1);
|
|
125
|
-
return Object.keys(t).length === 0 ? null : t;
|
|
126
|
-
}, ne = (e, r) => e?.key === r?.key && e?.rowId === r?.rowId && e?.value === r?.value, T = (e) => (r, t) => {
|
|
127
|
-
const n = r === void 0 ? null : e(r), s = t === void 0 ? null : e(t);
|
|
128
|
-
return ne(n, s) ? { remove: null, add: null } : { remove: n, add: s };
|
|
129
|
-
}, he = T(X), me = T(Z), w = (e, r, t) => {
|
|
130
|
-
if (t === null) {
|
|
131
|
-
const n = { ...e };
|
|
132
|
-
return delete n[r.key], n;
|
|
133
|
-
}
|
|
134
|
-
return { ...e, [r.key]: t };
|
|
135
|
-
}, Ie = (e, r) => {
|
|
136
|
-
if (r.remove === null && r.add === null) return e;
|
|
137
|
-
let t = e;
|
|
138
|
-
return r.remove !== null && (t = w(t, r.remove, te(t[r.remove.key], r.remove))), r.add !== null && (t = w(t, r.add, re(t[r.add.key], r.add))), t;
|
|
139
|
-
}, se = 1, g = (e) => typeof e == "number" && Number.isInteger(e) && e >= 0, oe = (e) => typeof e == "object" && e !== null && typeof e.id == "string" && typeof e.type == "string", _e = (e) => {
|
|
140
|
-
if (typeof e != "object" || e === null) return !1;
|
|
141
|
-
const r = e;
|
|
142
|
-
return r.format !== se || !g(r.position) || !g(r.revision) || !Array.isArray(r.entities) ? !1 : r.entities.every(oe);
|
|
143
|
-
};
|
|
144
|
-
export {
|
|
145
|
-
f as M,
|
|
146
|
-
se as R,
|
|
147
|
-
fe as a,
|
|
148
|
-
ie as b,
|
|
149
|
-
q as c,
|
|
150
|
-
ae as d,
|
|
151
|
-
X as e,
|
|
152
|
-
j as f,
|
|
153
|
-
Z as g,
|
|
154
|
-
Ie as h,
|
|
155
|
-
_e as i,
|
|
156
|
-
he as j,
|
|
157
|
-
me as k,
|
|
158
|
-
pe as l,
|
|
159
|
-
U as m,
|
|
160
|
-
ue as n,
|
|
161
|
-
de as o,
|
|
162
|
-
S as p,
|
|
163
|
-
te as q,
|
|
164
|
-
v as r,
|
|
165
|
-
re as s,
|
|
166
|
-
ce as w
|
|
167
|
-
};
|