@zeronsh/orbit 0.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/LICENSE +201 -0
- package/dist/adapter-C-AhY9cw.d.ts +18 -0
- package/dist/chunk-2R6QPZNI.js +274 -0
- package/dist/chunk-2R6QPZNI.js.map +1 -0
- package/dist/chunk-7CMFTRDQ.js +253 -0
- package/dist/chunk-7CMFTRDQ.js.map +1 -0
- package/dist/chunk-N2NAKHMU.js +1951 -0
- package/dist/chunk-N2NAKHMU.js.map +1 -0
- package/dist/chunk-QR3NSGHJ.js +131 -0
- package/dist/chunk-QR3NSGHJ.js.map +1 -0
- package/dist/client.d.ts +349 -0
- package/dist/client.js +3 -0
- package/dist/client.js.map +1 -0
- package/dist/custom-DzMQ-nY2.d.ts +81 -0
- package/dist/drizzle/cli/bin.d.ts +1 -0
- package/dist/drizzle/cli/bin.js +51 -0
- package/dist/drizzle/cli/bin.js.map +1 -0
- package/dist/drizzle/cli.d.ts +63 -0
- package/dist/drizzle/cli.js +6 -0
- package/dist/drizzle/cli.js.map +1 -0
- package/dist/drizzle.d.ts +21 -0
- package/dist/drizzle.js +20 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/introspect-zNCdXfuc.d.ts +31 -0
- package/dist/ir-DE_CZz0D.d.ts +49 -0
- package/dist/orm-core.d.ts +33 -0
- package/dist/orm-core.js +4 -0
- package/dist/orm-core.js.map +1 -0
- package/dist/query-BMK1cXAS.d.ts +296 -0
- package/dist/react.d.ts +16 -0
- package/dist/react.js +27 -0
- package/dist/react.js.map +1 -0
- package/dist/schema-BNM6bks7.d.ts +108 -0
- package/dist/server/pg.d.ts +18 -0
- package/dist/server/pg.js +35 -0
- package/dist/server/pg.js.map +1 -0
- package/dist/server.d.ts +41 -0
- package/dist/server.js +81 -0
- package/dist/server.js.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,1951 @@
|
|
|
1
|
+
// client/src/protocol.ts
|
|
2
|
+
var PROTOCOL_VERSION = 51;
|
|
3
|
+
function hashString(s) {
|
|
4
|
+
let h = 0;
|
|
5
|
+
for (let i = 0; i < s.length; i++) {
|
|
6
|
+
h = Math.imul(31, h) + s.charCodeAt(i) | 0;
|
|
7
|
+
}
|
|
8
|
+
return (h >>> 0).toString(36);
|
|
9
|
+
}
|
|
10
|
+
function hashAST(ast) {
|
|
11
|
+
return hashString(JSON.stringify(ast));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// client/src/query.ts
|
|
15
|
+
var Query = class _Query {
|
|
16
|
+
#ast;
|
|
17
|
+
/** `.one()` was called — a related parent should treat this as singular. */
|
|
18
|
+
#singular;
|
|
19
|
+
constructor(ast, singular = false) {
|
|
20
|
+
this.#ast = ast;
|
|
21
|
+
this.#singular = singular;
|
|
22
|
+
}
|
|
23
|
+
static from(table2) {
|
|
24
|
+
return new _Query({ table: table2 });
|
|
25
|
+
}
|
|
26
|
+
where(field, op, value) {
|
|
27
|
+
const cond = {
|
|
28
|
+
type: "simple",
|
|
29
|
+
op,
|
|
30
|
+
left: { type: "column", name: field },
|
|
31
|
+
right: { type: "literal", value }
|
|
32
|
+
};
|
|
33
|
+
return new _Query({ ...this.#ast, where: and(this.#ast.where, cond) }, this.#singular);
|
|
34
|
+
}
|
|
35
|
+
whereExists(correlation, subquery, negated = false) {
|
|
36
|
+
const cond = {
|
|
37
|
+
type: "correlatedSubquery",
|
|
38
|
+
related: { correlation, subquery: subquery.ast() },
|
|
39
|
+
op: negated ? "NOT EXISTS" : "EXISTS"
|
|
40
|
+
};
|
|
41
|
+
return new _Query({ ...this.#ast, where: and(this.#ast.where, cond) }, this.#singular);
|
|
42
|
+
}
|
|
43
|
+
related(name, correlation, subquery) {
|
|
44
|
+
const sub = { ...subquery.ast(), alias: name };
|
|
45
|
+
const entry = { correlation, subquery: sub, singular: subquery.#singular || void 0 };
|
|
46
|
+
const related = [...this.#ast.related ?? [], entry];
|
|
47
|
+
return new _Query({ ...this.#ast, related }, this.#singular);
|
|
48
|
+
}
|
|
49
|
+
/** Append a fully-formed related entry (used for junction/`hidden` chains). */
|
|
50
|
+
addRelated(entry) {
|
|
51
|
+
const related = [...this.#ast.related ?? [], entry];
|
|
52
|
+
return new _Query({ ...this.#ast, related }, this.#singular);
|
|
53
|
+
}
|
|
54
|
+
orderBy(field, dir) {
|
|
55
|
+
const orderBy = [...this.#ast.orderBy ?? [], [field, dir]];
|
|
56
|
+
return new _Query({ ...this.#ast, orderBy }, this.#singular);
|
|
57
|
+
}
|
|
58
|
+
limit(n) {
|
|
59
|
+
return new _Query({ ...this.#ast, limit: n }, this.#singular);
|
|
60
|
+
}
|
|
61
|
+
one() {
|
|
62
|
+
return new _Query({ ...this.#ast, limit: 1 }, true);
|
|
63
|
+
}
|
|
64
|
+
start(row, exclusive = false) {
|
|
65
|
+
return new _Query({ ...this.#ast, start: { row, exclusive } }, this.#singular);
|
|
66
|
+
}
|
|
67
|
+
/** Whether this query was marked `.one()` (singular). */
|
|
68
|
+
isSingular() {
|
|
69
|
+
return this.#singular;
|
|
70
|
+
}
|
|
71
|
+
ast() {
|
|
72
|
+
return this.#ast;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function and(existing, next) {
|
|
76
|
+
if (!existing) return next;
|
|
77
|
+
if (existing.type === "and") return { type: "and", conditions: [...existing.conditions, next] };
|
|
78
|
+
return { type: "and", conditions: [existing, next] };
|
|
79
|
+
}
|
|
80
|
+
var TypedQuery = class _TypedQuery {
|
|
81
|
+
#host;
|
|
82
|
+
#q;
|
|
83
|
+
constructor(host, q) {
|
|
84
|
+
this.#host = host;
|
|
85
|
+
this.#q = q;
|
|
86
|
+
}
|
|
87
|
+
where(field, op, value) {
|
|
88
|
+
return new _TypedQuery(this.#host, this.#q.where(field, op, value));
|
|
89
|
+
}
|
|
90
|
+
whereExists(correlation, subquery, negated = false) {
|
|
91
|
+
const sub = subquery instanceof _TypedQuery ? subquery.query() : subquery;
|
|
92
|
+
return new _TypedQuery(this.#host, this.#q.whereExists(correlation, sub, negated));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Add a nested relationship. The result type gains `name`: a `R[]` array, or
|
|
96
|
+
* `R | undefined` if the child query was `.one()`.
|
|
97
|
+
*/
|
|
98
|
+
related(name, correlation, subquery) {
|
|
99
|
+
return new _TypedQuery(this.#host, this.#q.related(name, correlation, subquery.query()));
|
|
100
|
+
}
|
|
101
|
+
orderBy(field, dir) {
|
|
102
|
+
return new _TypedQuery(this.#host, this.#q.orderBy(field, dir));
|
|
103
|
+
}
|
|
104
|
+
limit(n) {
|
|
105
|
+
return new _TypedQuery(this.#host, this.#q.limit(n));
|
|
106
|
+
}
|
|
107
|
+
one() {
|
|
108
|
+
return new _TypedQuery(this.#host, this.#q.one());
|
|
109
|
+
}
|
|
110
|
+
start(row, exclusive = false) {
|
|
111
|
+
return new _TypedQuery(this.#host, this.#q.start(row, exclusive));
|
|
112
|
+
}
|
|
113
|
+
/** The underlying untyped query (escape hatch). */
|
|
114
|
+
query() {
|
|
115
|
+
return this.#q;
|
|
116
|
+
}
|
|
117
|
+
ast() {
|
|
118
|
+
return this.#q.ast();
|
|
119
|
+
}
|
|
120
|
+
/** Materialize into a live, typed view. */
|
|
121
|
+
materialize() {
|
|
122
|
+
if (!this.#host) throw new Error("this query is an unbound builder; subscribe via orbit.query.<name>()");
|
|
123
|
+
return this.#host.materialize(this.#q);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var SchemaQuery = class _SchemaQuery {
|
|
127
|
+
#host;
|
|
128
|
+
#schema;
|
|
129
|
+
#table;
|
|
130
|
+
#q;
|
|
131
|
+
constructor(host, schema, table2, q) {
|
|
132
|
+
this.#host = host;
|
|
133
|
+
this.#schema = schema;
|
|
134
|
+
this.#table = table2;
|
|
135
|
+
this.#q = q;
|
|
136
|
+
}
|
|
137
|
+
#wrap(q) {
|
|
138
|
+
return new _SchemaQuery(this.#host, this.#schema, this.#table, q);
|
|
139
|
+
}
|
|
140
|
+
where(field, op, value) {
|
|
141
|
+
return this.#wrap(this.#q.where(field, op, value));
|
|
142
|
+
}
|
|
143
|
+
whereExists(correlation, subquery, negated = false) {
|
|
144
|
+
const sub = subquery instanceof _SchemaQuery ? subquery.query() : subquery;
|
|
145
|
+
return this.#wrap(this.#q.whereExists(correlation, sub, negated));
|
|
146
|
+
}
|
|
147
|
+
orderBy(field, dir) {
|
|
148
|
+
return this.#wrap(this.#q.orderBy(field, dir));
|
|
149
|
+
}
|
|
150
|
+
limit(n) {
|
|
151
|
+
return this.#wrap(this.#q.limit(n));
|
|
152
|
+
}
|
|
153
|
+
one() {
|
|
154
|
+
return this.#wrap(this.#q.one());
|
|
155
|
+
}
|
|
156
|
+
start(row, exclusive = false) {
|
|
157
|
+
return this.#wrap(this.#q.start(row, exclusive));
|
|
158
|
+
}
|
|
159
|
+
related(name, second, third) {
|
|
160
|
+
if (third !== void 0 || isCorrelation(second)) {
|
|
161
|
+
const correlation = second;
|
|
162
|
+
const sub = third;
|
|
163
|
+
const subq = sub instanceof Query ? sub : sub.query();
|
|
164
|
+
return this.#wrap(this.#q.related(name, correlation, subq));
|
|
165
|
+
}
|
|
166
|
+
const chain = this.#schema.relationships?.[this.#table]?.[name];
|
|
167
|
+
if (!chain) {
|
|
168
|
+
throw new Error(`Unknown relationship "${name}" on table "${this.#table}". Declare it with relationships() in the schema.`);
|
|
169
|
+
}
|
|
170
|
+
const cb = second;
|
|
171
|
+
const last = chain[chain.length - 1];
|
|
172
|
+
let dest = new _SchemaQuery(null, this.#schema, last.destSchema, Query.from(last.destSchema));
|
|
173
|
+
if (cb) dest = cb(dest);
|
|
174
|
+
let destAst = { ...dest.ast(), alias: name };
|
|
175
|
+
const destSingular = last.cardinality === "one" || dest.isSingular();
|
|
176
|
+
if (chain.length === 1) {
|
|
177
|
+
const entry2 = {
|
|
178
|
+
correlation: { parentField: [...last.sourceField], childField: [...last.destField] },
|
|
179
|
+
subquery: destAst,
|
|
180
|
+
singular: destSingular || void 0
|
|
181
|
+
};
|
|
182
|
+
return this.#wrap(this.#q.addRelated(entry2));
|
|
183
|
+
}
|
|
184
|
+
const [first, second2] = chain;
|
|
185
|
+
const junctionAst = {
|
|
186
|
+
table: first.destSchema,
|
|
187
|
+
alias: name,
|
|
188
|
+
related: [
|
|
189
|
+
{
|
|
190
|
+
correlation: { parentField: [...second2.sourceField], childField: [...second2.destField] },
|
|
191
|
+
subquery: destAst,
|
|
192
|
+
singular: destSingular || void 0
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
};
|
|
196
|
+
const entry = {
|
|
197
|
+
correlation: { parentField: [...first.sourceField], childField: [...first.destField] },
|
|
198
|
+
hidden: true,
|
|
199
|
+
subquery: junctionAst
|
|
200
|
+
};
|
|
201
|
+
return this.#wrap(this.#q.addRelated(entry));
|
|
202
|
+
}
|
|
203
|
+
isSingular() {
|
|
204
|
+
return this.#q.isSingular();
|
|
205
|
+
}
|
|
206
|
+
/** The underlying untyped query (escape hatch). */
|
|
207
|
+
query() {
|
|
208
|
+
return this.#q;
|
|
209
|
+
}
|
|
210
|
+
ast() {
|
|
211
|
+
return this.#q.ast();
|
|
212
|
+
}
|
|
213
|
+
materialize() {
|
|
214
|
+
if (!this.#host) throw new Error("this query is an unbound builder; subscribe via orbit.query.<name>()");
|
|
215
|
+
return this.#host.materialize(this.#q);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
function isCorrelation(v) {
|
|
219
|
+
return typeof v === "object" && v !== null && "parentField" in v && "childField" in v;
|
|
220
|
+
}
|
|
221
|
+
function createBuilder(schema) {
|
|
222
|
+
return buildSchemaQueries(schema, null);
|
|
223
|
+
}
|
|
224
|
+
function buildSchemaQueries(schema, host) {
|
|
225
|
+
const out = {};
|
|
226
|
+
for (const name of Object.keys(schema.tables)) {
|
|
227
|
+
out[name] = new SchemaQuery(host, schema, name, Query.from(name));
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// client/src/schema.ts
|
|
233
|
+
var string = () => ({ type: "string", optional: false });
|
|
234
|
+
var number = () => ({ type: "number", optional: false });
|
|
235
|
+
var boolean = () => ({ type: "boolean", optional: false });
|
|
236
|
+
var json = () => ({ type: "json", optional: false });
|
|
237
|
+
var optional = (c) => ({ ...c, optional: true });
|
|
238
|
+
function table(name) {
|
|
239
|
+
return {
|
|
240
|
+
columns(columns) {
|
|
241
|
+
return {
|
|
242
|
+
primaryKey(...primaryKey) {
|
|
243
|
+
return { name, columns, primaryKey };
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
var makeConnector = (cardinality) => (...args) => args.map((a) => ({
|
|
250
|
+
sourceField: a.sourceField,
|
|
251
|
+
destField: a.destField,
|
|
252
|
+
destSchema: a.destSchema.name,
|
|
253
|
+
cardinality
|
|
254
|
+
}));
|
|
255
|
+
function relationships(table2, cb) {
|
|
256
|
+
const r = cb({
|
|
257
|
+
one: makeConnector("one"),
|
|
258
|
+
many: makeConnector("many")
|
|
259
|
+
});
|
|
260
|
+
return { name: table2.name, relationships: r };
|
|
261
|
+
}
|
|
262
|
+
function createSchema(def) {
|
|
263
|
+
const tables = {};
|
|
264
|
+
for (const t of def.tables) tables[t.name] = t;
|
|
265
|
+
const rels = {};
|
|
266
|
+
for (const r of def.relationships ?? []) rels[r.name] = r.relationships;
|
|
267
|
+
return { tables, relationships: rels };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// client/src/custom.ts
|
|
271
|
+
function defineMutator(fn) {
|
|
272
|
+
return fn;
|
|
273
|
+
}
|
|
274
|
+
function defineQuery(fn) {
|
|
275
|
+
return fn;
|
|
276
|
+
}
|
|
277
|
+
function collectOps(schema, def, args, ctx) {
|
|
278
|
+
const pk = {};
|
|
279
|
+
for (const t of Object.values(schema.tables)) pk[t.name] = [...t.primaryKey];
|
|
280
|
+
const ops = [];
|
|
281
|
+
const mutate = new Proxy(
|
|
282
|
+
{},
|
|
283
|
+
{
|
|
284
|
+
get: (_t, table2) => {
|
|
285
|
+
const make = (op) => (value) => ops.push({ op, tableName: table2, primaryKey: pk[table2] ?? ["id"], value });
|
|
286
|
+
return { insert: make("insert"), upsert: make("upsert"), update: make("update"), delete: make("delete") };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
const tx = { location: "client", mutate };
|
|
291
|
+
void def({ tx, args, ctx });
|
|
292
|
+
return ops;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// client/src/query-manager.ts
|
|
296
|
+
var UNIT_MS = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
|
|
297
|
+
var MAX_TTL_MS = 6e5;
|
|
298
|
+
var DEFAULT_TTL = "5m";
|
|
299
|
+
function parseTTL(ttl) {
|
|
300
|
+
if (ttl === "forever") return Infinity;
|
|
301
|
+
if (ttl === "none") return 0;
|
|
302
|
+
if (typeof ttl === "number") return Math.max(0, ttl);
|
|
303
|
+
const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(ttl);
|
|
304
|
+
return m ? Number(m[1]) * UNIT_MS[m[2]] : parseTTL(DEFAULT_TTL);
|
|
305
|
+
}
|
|
306
|
+
function clampTTL(ms) {
|
|
307
|
+
return ms === Infinity ? Infinity : Math.min(ms, MAX_TTL_MS);
|
|
308
|
+
}
|
|
309
|
+
var defaultScheduler = {
|
|
310
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
311
|
+
clearTimeout: (h) => clearTimeout(h)
|
|
312
|
+
};
|
|
313
|
+
var QueryManager = class {
|
|
314
|
+
#entries = /* @__PURE__ */ new Map();
|
|
315
|
+
#onSubscribe;
|
|
316
|
+
#onUnsubscribe;
|
|
317
|
+
#sched;
|
|
318
|
+
constructor(opts) {
|
|
319
|
+
this.#onSubscribe = opts.onSubscribe;
|
|
320
|
+
this.#onUnsubscribe = opts.onUnsubscribe;
|
|
321
|
+
this.#sched = opts.scheduler ?? defaultScheduler;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Register interest in a query. Returns a release function (idempotent). The
|
|
325
|
+
* first registration subscribes upstream; identical queries dedupe to one.
|
|
326
|
+
*/
|
|
327
|
+
add(hash, put, ttl = DEFAULT_TTL) {
|
|
328
|
+
const ttlMs = clampTTL(parseTTL(ttl));
|
|
329
|
+
let entry = this.#entries.get(hash);
|
|
330
|
+
if (entry) {
|
|
331
|
+
entry.count++;
|
|
332
|
+
entry.ttlMs = Math.max(entry.ttlMs, ttlMs);
|
|
333
|
+
this.#cancelGc(entry);
|
|
334
|
+
} else {
|
|
335
|
+
const stamped = Number.isFinite(ttlMs) ? { ...put, ttl: ttlMs } : put;
|
|
336
|
+
entry = { put: stamped, count: 1, ttlMs };
|
|
337
|
+
this.#entries.set(hash, entry);
|
|
338
|
+
this.#onSubscribe(stamped);
|
|
339
|
+
}
|
|
340
|
+
let released = false;
|
|
341
|
+
return () => {
|
|
342
|
+
if (released) return;
|
|
343
|
+
released = true;
|
|
344
|
+
this.#release(hash);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
#release(hash) {
|
|
348
|
+
const entry = this.#entries.get(hash);
|
|
349
|
+
if (!entry || --entry.count > 0) return;
|
|
350
|
+
if (entry.ttlMs === Infinity) return;
|
|
351
|
+
if (entry.ttlMs <= 0) {
|
|
352
|
+
this.#gc(hash);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
entry.timer = this.#sched.setTimeout(() => this.#gc(hash), entry.ttlMs);
|
|
356
|
+
}
|
|
357
|
+
#cancelGc(entry) {
|
|
358
|
+
if (entry.timer !== void 0) {
|
|
359
|
+
this.#sched.clearTimeout(entry.timer);
|
|
360
|
+
entry.timer = void 0;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
#gc(hash) {
|
|
364
|
+
const entry = this.#entries.get(hash);
|
|
365
|
+
if (!entry || entry.count > 0) return;
|
|
366
|
+
this.#entries.delete(hash);
|
|
367
|
+
this.#onUnsubscribe(hash);
|
|
368
|
+
}
|
|
369
|
+
/** All currently-subscribed query `put` ops (for reconnect resume). */
|
|
370
|
+
active() {
|
|
371
|
+
return [...this.#entries.values()].map((e) => e.put);
|
|
372
|
+
}
|
|
373
|
+
/** Number of live (subscribed, incl. within-TTL) queries — for tests/introspection. */
|
|
374
|
+
size() {
|
|
375
|
+
return this.#entries.size;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// client/src/store.ts
|
|
380
|
+
var Store = class {
|
|
381
|
+
/** table -> (pk-json -> row), server-synced. */
|
|
382
|
+
#tables = /* @__PURE__ */ new Map();
|
|
383
|
+
/** Optimistic, not-yet-confirmed mutations, in order. */
|
|
384
|
+
#pending = [];
|
|
385
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
386
|
+
#pkByTable;
|
|
387
|
+
// persistence
|
|
388
|
+
#kv;
|
|
389
|
+
#dirtyRows = /* @__PURE__ */ new Set();
|
|
390
|
+
// "tablepkKey"
|
|
391
|
+
#dirtyPending = /* @__PURE__ */ new Set();
|
|
392
|
+
#cleared = false;
|
|
393
|
+
#flushTimer;
|
|
394
|
+
/** Effective-row keys touched since the last notify (delivered to listeners). */
|
|
395
|
+
#changed = [];
|
|
396
|
+
constructor(pkByTable = {}) {
|
|
397
|
+
this.#pkByTable = pkByTable;
|
|
398
|
+
}
|
|
399
|
+
#pkCols(table2) {
|
|
400
|
+
return this.#pkByTable[table2] ?? ["id"];
|
|
401
|
+
}
|
|
402
|
+
/** Primary-key columns for a table (for complete ordering in the evaluator). */
|
|
403
|
+
pkOf(table2) {
|
|
404
|
+
return this.#pkCols(table2);
|
|
405
|
+
}
|
|
406
|
+
/** Primary-key tuple in *declared* order — matches the IVM engine's `pkKey`. */
|
|
407
|
+
#key(table2, row) {
|
|
408
|
+
return JSON.stringify(this.#pkCols(table2).map((c) => row[c] ?? null));
|
|
409
|
+
}
|
|
410
|
+
/** Public pk-key for a row (used by the IVM-backed view to match changes). */
|
|
411
|
+
keyOf(table2, row) {
|
|
412
|
+
return this.#key(table2, row);
|
|
413
|
+
}
|
|
414
|
+
/** The effective (synced + optimistic overlay) row for a pk key, or undefined. */
|
|
415
|
+
effectiveRow(table2, key) {
|
|
416
|
+
let row = this.#table(table2).get(key);
|
|
417
|
+
for (const p of this.#pending) {
|
|
418
|
+
for (const op of p.ops) {
|
|
419
|
+
if (op.tableName !== table2 || this.#key(table2, op.value) !== key) continue;
|
|
420
|
+
if (op.op === "delete") row = void 0;
|
|
421
|
+
else if (op.op === "update") row = { ...row ?? {}, ...op.value };
|
|
422
|
+
else row = op.value;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return row;
|
|
426
|
+
}
|
|
427
|
+
#table(name) {
|
|
428
|
+
let t = this.#tables.get(name);
|
|
429
|
+
if (!t) {
|
|
430
|
+
t = /* @__PURE__ */ new Map();
|
|
431
|
+
this.#tables.set(name, t);
|
|
432
|
+
}
|
|
433
|
+
return t;
|
|
434
|
+
}
|
|
435
|
+
// --- server-synced state (from pokes) ------------------------------------
|
|
436
|
+
apply(op) {
|
|
437
|
+
switch (op.op) {
|
|
438
|
+
case "put": {
|
|
439
|
+
const key = this.#key(op.tableName, op.value);
|
|
440
|
+
this.#table(op.tableName).set(key, op.value);
|
|
441
|
+
this.#touch(op.tableName, key);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
case "update": {
|
|
445
|
+
const t = this.#table(op.tableName);
|
|
446
|
+
const key = this.#key(op.tableName, op.id);
|
|
447
|
+
const existing = t.get(key) ?? { ...op.id };
|
|
448
|
+
t.set(key, { ...existing, ...op.merge ?? {} });
|
|
449
|
+
this.#touch(op.tableName, key);
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case "del": {
|
|
453
|
+
const key = this.#key(op.tableName, op.id);
|
|
454
|
+
this.#table(op.tableName).delete(key);
|
|
455
|
+
this.#touch(op.tableName, key);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
case "clear":
|
|
459
|
+
this.#tables.clear();
|
|
460
|
+
this.#cleared = true;
|
|
461
|
+
this.#dirtyRows.clear();
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
applyAll(ops) {
|
|
466
|
+
for (const op of ops) this.apply(op);
|
|
467
|
+
this.#scheduleFlush();
|
|
468
|
+
this.#notify();
|
|
469
|
+
}
|
|
470
|
+
// --- optimistic overlay --------------------------------------------------
|
|
471
|
+
/** Queue an optimistic mutation's CRUD ops (applied on top of synced rows). */
|
|
472
|
+
addPending(id, ops, mutation) {
|
|
473
|
+
this.#pending.push({ id, ops, mutation });
|
|
474
|
+
this.#dirtyPending.add(id);
|
|
475
|
+
for (const op of ops) this.#touch(op.tableName, this.#key(op.tableName, op.value));
|
|
476
|
+
this.#scheduleFlush();
|
|
477
|
+
this.#notify();
|
|
478
|
+
}
|
|
479
|
+
/** Drop pending mutations the server has confirmed (id <= confirmed). */
|
|
480
|
+
confirmThrough(id) {
|
|
481
|
+
const before = this.#pending.length;
|
|
482
|
+
const dropped = this.#pending.filter((p) => p.id <= id);
|
|
483
|
+
if (dropped.length === 0) return;
|
|
484
|
+
this.#pending = this.#pending.filter((p) => p.id > id);
|
|
485
|
+
for (const p of dropped) {
|
|
486
|
+
this.#dirtyPending.add(p.id);
|
|
487
|
+
for (const op of p.ops) this.#touch(op.tableName, this.#key(op.tableName, op.value));
|
|
488
|
+
}
|
|
489
|
+
if (this.#pending.length !== before) {
|
|
490
|
+
this.#scheduleFlush();
|
|
491
|
+
this.#notify();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/** Originating messages of still-pending mutations (for resend after reload). */
|
|
495
|
+
pendingMutations() {
|
|
496
|
+
return this.#pending.map((p) => p.mutation).filter((m) => m !== void 0);
|
|
497
|
+
}
|
|
498
|
+
/** All rows for `table` with the optimistic overlay applied. */
|
|
499
|
+
effectiveRows(table2) {
|
|
500
|
+
const merged = new Map(this.#table(table2));
|
|
501
|
+
for (const p of this.#pending) {
|
|
502
|
+
for (const op of p.ops) {
|
|
503
|
+
if (op.tableName !== table2) continue;
|
|
504
|
+
const key = this.#key(table2, op.value);
|
|
505
|
+
if (op.op === "delete") merged.delete(key);
|
|
506
|
+
else if (op.op === "update") merged.set(key, { ...merged.get(key) ?? {}, ...op.value });
|
|
507
|
+
else merged.set(key, op.value);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return [...merged.values()];
|
|
511
|
+
}
|
|
512
|
+
subscribe(fn) {
|
|
513
|
+
this.#listeners.add(fn);
|
|
514
|
+
return () => this.#listeners.delete(fn);
|
|
515
|
+
}
|
|
516
|
+
#notify() {
|
|
517
|
+
const changed = this.#changed;
|
|
518
|
+
this.#changed = [];
|
|
519
|
+
for (const fn of this.#listeners) fn(changed);
|
|
520
|
+
}
|
|
521
|
+
// --- persistence ---------------------------------------------------------
|
|
522
|
+
#touch(table2, pkKey2) {
|
|
523
|
+
this.#changed.push({ table: table2, key: pkKey2 });
|
|
524
|
+
this.#dirtyRows.add(`${table2}\0${pkKey2}`);
|
|
525
|
+
}
|
|
526
|
+
#scheduleFlush() {
|
|
527
|
+
if (!this.#kv || this.#flushTimer !== void 0) return;
|
|
528
|
+
this.#flushTimer = setTimeout(() => void this.flush(), 50);
|
|
529
|
+
}
|
|
530
|
+
/** Load persisted rows + pending mutations from `kv` into memory. */
|
|
531
|
+
async hydrate(kv) {
|
|
532
|
+
this.#kv = kv;
|
|
533
|
+
const loaded = [];
|
|
534
|
+
for (const [, val] of await kv.entries("e/")) {
|
|
535
|
+
const { t, k, v } = val;
|
|
536
|
+
this.#table(t).set(k, v);
|
|
537
|
+
loaded.push({ table: t, key: k });
|
|
538
|
+
}
|
|
539
|
+
const pend = (await kv.entries("p/")).map(([, v]) => v).sort((a, b) => a.id - b.id);
|
|
540
|
+
for (const p of pend) {
|
|
541
|
+
this.#pending.push(p);
|
|
542
|
+
for (const op of p.ops) loaded.push({ table: op.tableName, key: this.#key(op.tableName, op.value) });
|
|
543
|
+
}
|
|
544
|
+
this.#changed.push(...loaded);
|
|
545
|
+
this.#notify();
|
|
546
|
+
}
|
|
547
|
+
/** Flush dirty rows + pending mutations to the KV (called debounced). */
|
|
548
|
+
async flush() {
|
|
549
|
+
if (this.#flushTimer !== void 0) {
|
|
550
|
+
clearTimeout(this.#flushTimer);
|
|
551
|
+
this.#flushTimer = void 0;
|
|
552
|
+
}
|
|
553
|
+
const kv = this.#kv;
|
|
554
|
+
if (!kv) return;
|
|
555
|
+
if (this.#cleared) {
|
|
556
|
+
const dels = [];
|
|
557
|
+
for (const [k] of await kv.entries("e/")) dels.push(kv.del(k));
|
|
558
|
+
await Promise.all(dels);
|
|
559
|
+
this.#cleared = false;
|
|
560
|
+
}
|
|
561
|
+
const ps = [];
|
|
562
|
+
for (const dk of this.#dirtyRows) {
|
|
563
|
+
const sep = dk.indexOf("\0");
|
|
564
|
+
const table2 = dk.slice(0, sep);
|
|
565
|
+
const pkKey2 = dk.slice(sep + 1);
|
|
566
|
+
const row = this.#tables.get(table2)?.get(pkKey2);
|
|
567
|
+
const key = `e/${table2}/${pkKey2}`;
|
|
568
|
+
ps.push(row ? kv.set(key, { t: table2, k: pkKey2, v: row }) : kv.del(key));
|
|
569
|
+
}
|
|
570
|
+
this.#dirtyRows.clear();
|
|
571
|
+
for (const id of this.#dirtyPending) {
|
|
572
|
+
const p = this.#pending.find((x) => x.id === id);
|
|
573
|
+
ps.push(p ? kv.set(`p/${id}`, p) : kv.del(`p/${id}`));
|
|
574
|
+
}
|
|
575
|
+
this.#dirtyPending.clear();
|
|
576
|
+
await Promise.all(ps);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// client/src/eval.ts
|
|
581
|
+
function isNull(v) {
|
|
582
|
+
return v === null || v === void 0;
|
|
583
|
+
}
|
|
584
|
+
function typeRank(v) {
|
|
585
|
+
if (isNull(v)) return 0;
|
|
586
|
+
switch (typeof v) {
|
|
587
|
+
case "boolean":
|
|
588
|
+
return 1;
|
|
589
|
+
case "number":
|
|
590
|
+
return 2;
|
|
591
|
+
case "string":
|
|
592
|
+
return 3;
|
|
593
|
+
default:
|
|
594
|
+
return 4;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function compareValues(a, b) {
|
|
598
|
+
if (isNull(a) && isNull(b)) return 0;
|
|
599
|
+
if (isNull(a)) return -1;
|
|
600
|
+
if (isNull(b)) return 1;
|
|
601
|
+
if (typeof a === typeof b) {
|
|
602
|
+
if (typeof a === "number") return a < b ? -1 : a > b ? 1 : 0;
|
|
603
|
+
if (typeof a === "boolean") return a === b ? 0 : a ? 1 : -1;
|
|
604
|
+
if (typeof a === "string") return a < b ? -1 : a > b ? 1 : 0;
|
|
605
|
+
return 0;
|
|
606
|
+
}
|
|
607
|
+
return typeRank(a) - typeRank(b);
|
|
608
|
+
}
|
|
609
|
+
function valuesEqual(a, b) {
|
|
610
|
+
if (isNull(a) || isNull(b)) return false;
|
|
611
|
+
return a === b;
|
|
612
|
+
}
|
|
613
|
+
function likeToRegExp(pattern, ci) {
|
|
614
|
+
let re = "";
|
|
615
|
+
for (const ch of pattern) {
|
|
616
|
+
if (ch === "%") re += ".*";
|
|
617
|
+
else if (ch === "_") re += ".";
|
|
618
|
+
else re += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
619
|
+
}
|
|
620
|
+
return new RegExp(`^${re}$`, ci ? "is" : "s");
|
|
621
|
+
}
|
|
622
|
+
function resolve(row, pos) {
|
|
623
|
+
if (pos.type === "column") return row[pos.name];
|
|
624
|
+
if (pos.type === "literal") return pos.value;
|
|
625
|
+
return void 0;
|
|
626
|
+
}
|
|
627
|
+
function evalSimple(row, op, left, right) {
|
|
628
|
+
const l = resolve(row, left);
|
|
629
|
+
const r = resolve(row, right);
|
|
630
|
+
if (l === void 0 || r === void 0) return true;
|
|
631
|
+
if (op === "IS") return isNull(l) === isNull(r) && (isNull(l) || l === r);
|
|
632
|
+
if (op === "IS NOT") return !(isNull(l) === isNull(r) && (isNull(l) || l === r));
|
|
633
|
+
if (op === "IN" || op === "NOT IN") {
|
|
634
|
+
if (isNull(l)) return false;
|
|
635
|
+
const arr = Array.isArray(r) ? r : [r];
|
|
636
|
+
const found = arr.some((x) => valuesEqual(l, x));
|
|
637
|
+
return op === "IN" ? found : !found;
|
|
638
|
+
}
|
|
639
|
+
const a = l;
|
|
640
|
+
const b = r;
|
|
641
|
+
switch (op) {
|
|
642
|
+
case "=":
|
|
643
|
+
return valuesEqual(a, b);
|
|
644
|
+
case "!=":
|
|
645
|
+
return !isNull(a) && !isNull(b) && a !== b;
|
|
646
|
+
case "<":
|
|
647
|
+
return !isNull(a) && !isNull(b) && compareValues(a, b) < 0;
|
|
648
|
+
case ">":
|
|
649
|
+
return !isNull(a) && !isNull(b) && compareValues(a, b) > 0;
|
|
650
|
+
case "<=":
|
|
651
|
+
return !isNull(a) && !isNull(b) && compareValues(a, b) <= 0;
|
|
652
|
+
case ">=":
|
|
653
|
+
return !isNull(a) && !isNull(b) && compareValues(a, b) >= 0;
|
|
654
|
+
case "LIKE":
|
|
655
|
+
return !isNull(a) && likeToRegExp(String(b), false).test(String(a));
|
|
656
|
+
case "NOT LIKE":
|
|
657
|
+
return !isNull(a) && !likeToRegExp(String(b), false).test(String(a));
|
|
658
|
+
case "ILIKE":
|
|
659
|
+
return !isNull(a) && likeToRegExp(String(b), true).test(String(a));
|
|
660
|
+
case "NOT ILIKE":
|
|
661
|
+
return !isNull(a) && !likeToRegExp(String(b), true).test(String(a));
|
|
662
|
+
default:
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function simpleMatches(row, cond) {
|
|
667
|
+
return evalSimple(row, cond.op, cond.left, cond.right);
|
|
668
|
+
}
|
|
669
|
+
function compareByOrder(a, b, orderBy) {
|
|
670
|
+
for (const [field, dir] of orderBy) {
|
|
671
|
+
const c = compareValues(a[field], b[field]);
|
|
672
|
+
if (c !== 0) return dir === "asc" ? c : -c;
|
|
673
|
+
}
|
|
674
|
+
return 0;
|
|
675
|
+
}
|
|
676
|
+
function completeOrder(orderBy, pk) {
|
|
677
|
+
const order = [...orderBy ?? []];
|
|
678
|
+
for (const k of pk) if (!order.some(([f]) => f === k)) order.push([k, "asc"]);
|
|
679
|
+
return order;
|
|
680
|
+
}
|
|
681
|
+
function correlated(parent, child, c) {
|
|
682
|
+
return c.parentField.every((pf, i) => valuesEqual(parent[pf], child[c.childField[i]]));
|
|
683
|
+
}
|
|
684
|
+
function existsRelationships(cond) {
|
|
685
|
+
const out = [];
|
|
686
|
+
const walk = (c) => {
|
|
687
|
+
if (!c) return;
|
|
688
|
+
if (c.type === "correlatedSubquery") {
|
|
689
|
+
const alias = c.related.subquery.alias;
|
|
690
|
+
if (alias && !alias.startsWith("zsubq_")) out.push(c.related);
|
|
691
|
+
} else if (c.type === "and" || c.type === "or") {
|
|
692
|
+
for (const inner of c.conditions) walk(inner);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
walk(cond);
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
function liftHidden(junctionRows, junctionAst) {
|
|
699
|
+
const inner = junctionAst.related?.[0];
|
|
700
|
+
if (!inner) return [];
|
|
701
|
+
const innerAlias = inner.subquery.alias ?? inner.subquery.table;
|
|
702
|
+
return junctionRows.flatMap((jr) => jr[innerAlias] ?? []);
|
|
703
|
+
}
|
|
704
|
+
function evaluate(getRows, ast, opts = {}) {
|
|
705
|
+
const applyWhere = opts.applyWhere ?? true;
|
|
706
|
+
const pkOf = opts.pkOf ?? (() => ["id"]);
|
|
707
|
+
const run = (node, parent, corr) => {
|
|
708
|
+
let rows = getRows(node.table);
|
|
709
|
+
if (parent && corr) rows = rows.filter((r) => correlated(parent, r, corr));
|
|
710
|
+
const evalCond = (r, cond) => {
|
|
711
|
+
switch (cond.type) {
|
|
712
|
+
case "simple":
|
|
713
|
+
return evalSimple(r, cond.op, cond.left, cond.right);
|
|
714
|
+
case "and":
|
|
715
|
+
return cond.conditions.every((c) => evalCond(r, c));
|
|
716
|
+
case "or":
|
|
717
|
+
return cond.conditions.some((c) => evalCond(r, c));
|
|
718
|
+
case "correlatedSubquery": {
|
|
719
|
+
const has = run(cond.related.subquery, r, cond.related.correlation).length > 0;
|
|
720
|
+
return cond.op === "EXISTS" ? has : !has;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
if (applyWhere && node.where) rows = rows.filter((r) => evalCond(r, node.where));
|
|
725
|
+
rows = [...rows];
|
|
726
|
+
const order = completeOrder(node.orderBy, pkOf(node.table));
|
|
727
|
+
rows.sort((a, b) => compareByOrder(a, b, order));
|
|
728
|
+
if (node.start) {
|
|
729
|
+
const { row: cursor, exclusive } = node.start;
|
|
730
|
+
const i = rows.findIndex((r) => {
|
|
731
|
+
const c = compareByOrder(r, cursor, order);
|
|
732
|
+
return exclusive ? c > 0 : c >= 0;
|
|
733
|
+
});
|
|
734
|
+
rows = i < 0 ? [] : rows.slice(i);
|
|
735
|
+
}
|
|
736
|
+
if (node.limit != null) rows = rows.slice(0, node.limit);
|
|
737
|
+
const related = node.related ?? [];
|
|
738
|
+
const existsRels = existsRelationships(node.where);
|
|
739
|
+
if (related.length === 0 && existsRels.length === 0) return rows;
|
|
740
|
+
return rows.map((r) => {
|
|
741
|
+
const out = { ...r };
|
|
742
|
+
for (const rel of [...related, ...existsRels]) {
|
|
743
|
+
const alias = rel.subquery.alias ?? rel.subquery.table;
|
|
744
|
+
const children = run(rel.subquery, r, rel.correlation);
|
|
745
|
+
out[alias] = rel.hidden ? liftHidden(children, rel.subquery) : children;
|
|
746
|
+
}
|
|
747
|
+
return out;
|
|
748
|
+
});
|
|
749
|
+
};
|
|
750
|
+
return run(ast, null, null);
|
|
751
|
+
}
|
|
752
|
+
function unwrapSingular(rows, ast) {
|
|
753
|
+
const related = ast.related;
|
|
754
|
+
if (!related || related.length === 0) return rows;
|
|
755
|
+
return rows.map((r) => {
|
|
756
|
+
const out = { ...r };
|
|
757
|
+
for (const rel of related) {
|
|
758
|
+
const alias = rel.subquery.alias ?? rel.subquery.table;
|
|
759
|
+
if (rel.hidden) {
|
|
760
|
+
const inner = rel.subquery.related[0];
|
|
761
|
+
const dest = unwrapSingular(out[alias] ?? [], inner.subquery);
|
|
762
|
+
out[alias] = inner.singular ? dest[0] : dest;
|
|
763
|
+
} else {
|
|
764
|
+
const children = unwrapSingular(out[alias] ?? [], rel.subquery);
|
|
765
|
+
out[alias] = rel.singular ? children[0] : children;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return out;
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// client/src/ivm/data.ts
|
|
773
|
+
function deliver(op, change) {
|
|
774
|
+
const results = op.push(change);
|
|
775
|
+
const out = op.output;
|
|
776
|
+
if (out) for (const r of results) deliver(out, r);
|
|
777
|
+
}
|
|
778
|
+
function makeComparator(order, reverse = false) {
|
|
779
|
+
return (a, b) => {
|
|
780
|
+
for (const [field, dir] of order) {
|
|
781
|
+
const c = compareValues(a[field], b[field]);
|
|
782
|
+
if (c !== 0) {
|
|
783
|
+
const r = dir === "asc" ? c : -c;
|
|
784
|
+
return reverse ? -r : r;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return 0;
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function completeOrder2(orderBy, pk) {
|
|
791
|
+
const order = [...orderBy ?? []];
|
|
792
|
+
for (const k of pk) if (!order.some(([f]) => f === k)) order.push([k, "asc"]);
|
|
793
|
+
return order;
|
|
794
|
+
}
|
|
795
|
+
function pkKey(pk, row) {
|
|
796
|
+
return JSON.stringify(pk.map((k) => row[k] ?? null));
|
|
797
|
+
}
|
|
798
|
+
function constraintMatches(constraint, row) {
|
|
799
|
+
for (const k of Object.keys(constraint)) {
|
|
800
|
+
if (!valuesEqual(row[k] ?? null, constraint[k])) return false;
|
|
801
|
+
}
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
function buildJoinConstraint(from, fromKeys, toKeys) {
|
|
805
|
+
const c = {};
|
|
806
|
+
for (let i = 0; i < fromKeys.length; i++) {
|
|
807
|
+
const v = from[fromKeys[i]] ?? null;
|
|
808
|
+
if (v === null) return null;
|
|
809
|
+
c[toKeys[i]] = v;
|
|
810
|
+
}
|
|
811
|
+
return c;
|
|
812
|
+
}
|
|
813
|
+
function rowEq(a, b) {
|
|
814
|
+
const ak = Object.keys(a);
|
|
815
|
+
const bk = Object.keys(b);
|
|
816
|
+
if (ak.length !== bk.length) return false;
|
|
817
|
+
for (const k of ak) if (a[k] !== b[k]) return false;
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
function nodeEq(a, b) {
|
|
821
|
+
if (!rowEq(a.row, b.row)) return false;
|
|
822
|
+
const ak = Object.keys(a.relationships);
|
|
823
|
+
const bk = Object.keys(b.relationships);
|
|
824
|
+
if (ak.length !== bk.length) return false;
|
|
825
|
+
for (const k of ak) {
|
|
826
|
+
const ca = a.relationships[k];
|
|
827
|
+
const cb = b.relationships[k];
|
|
828
|
+
if (!cb || ca.length !== cb.length) return false;
|
|
829
|
+
for (let i = 0; i < ca.length; i++) if (!nodeEq(ca[i], cb[i])) return false;
|
|
830
|
+
}
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// client/src/ivm/operators.ts
|
|
835
|
+
var Filter = class {
|
|
836
|
+
output = null;
|
|
837
|
+
#input;
|
|
838
|
+
#predicate;
|
|
839
|
+
constructor(input, predicate) {
|
|
840
|
+
this.#input = input;
|
|
841
|
+
this.#predicate = predicate;
|
|
842
|
+
input.setOutput(this);
|
|
843
|
+
}
|
|
844
|
+
fetch(req) {
|
|
845
|
+
return this.#input.fetch(req).filter((n) => this.#predicate(n.row));
|
|
846
|
+
}
|
|
847
|
+
push(change) {
|
|
848
|
+
return filterPush(change, this.#predicate);
|
|
849
|
+
}
|
|
850
|
+
setOutput(o) {
|
|
851
|
+
this.output = o;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
function filterPush(change, predicate) {
|
|
855
|
+
switch (change.type) {
|
|
856
|
+
case "add":
|
|
857
|
+
case "remove":
|
|
858
|
+
case "child":
|
|
859
|
+
return predicate(change.node.row) ? [change] : [];
|
|
860
|
+
case "edit": {
|
|
861
|
+
const o = predicate(change.oldNode.row);
|
|
862
|
+
const n = predicate(change.node.row);
|
|
863
|
+
if (o && n) return [change];
|
|
864
|
+
if (o && !n) return [{ type: "remove", node: change.oldNode }];
|
|
865
|
+
if (!o && n) return [{ type: "add", node: change.node }];
|
|
866
|
+
return [];
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function skip(input, bound, exclusive, cmp) {
|
|
871
|
+
return new Filter(input, (row) => {
|
|
872
|
+
const o = cmp(row, bound);
|
|
873
|
+
return exclusive ? o > 0 : o >= 0;
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
var CondFilter = class {
|
|
877
|
+
output = null;
|
|
878
|
+
#input;
|
|
879
|
+
#predicate;
|
|
880
|
+
#pk;
|
|
881
|
+
#passing = null;
|
|
882
|
+
constructor(input, predicate, pk) {
|
|
883
|
+
this.#input = input;
|
|
884
|
+
this.#predicate = predicate;
|
|
885
|
+
this.#pk = pk;
|
|
886
|
+
input.setOutput(this);
|
|
887
|
+
}
|
|
888
|
+
/** Lazily build the set of currently-passing primary keys from the input. */
|
|
889
|
+
#ensure() {
|
|
890
|
+
if (!this.#passing) {
|
|
891
|
+
this.#passing = /* @__PURE__ */ new Set();
|
|
892
|
+
for (const n of this.#input.fetch({})) if (this.#predicate(n)) this.#passing.add(pkKey(this.#pk, n.row));
|
|
893
|
+
}
|
|
894
|
+
return this.#passing;
|
|
895
|
+
}
|
|
896
|
+
fetch(req) {
|
|
897
|
+
const nodes = this.#input.fetch(req).filter((n) => this.#predicate(n));
|
|
898
|
+
this.#ensure();
|
|
899
|
+
return nodes;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Incremental: only the changed node's membership can flip, so process just
|
|
903
|
+
* this change instead of recomputing the whole passing set.
|
|
904
|
+
*/
|
|
905
|
+
push(change) {
|
|
906
|
+
const passing = this.#ensure();
|
|
907
|
+
const key = pkKey(this.#pk, change.node.row);
|
|
908
|
+
switch (change.type) {
|
|
909
|
+
case "add":
|
|
910
|
+
if (this.#predicate(change.node)) {
|
|
911
|
+
passing.add(key);
|
|
912
|
+
return [change];
|
|
913
|
+
}
|
|
914
|
+
return [];
|
|
915
|
+
case "remove":
|
|
916
|
+
if (passing.has(key)) {
|
|
917
|
+
passing.delete(key);
|
|
918
|
+
return [change];
|
|
919
|
+
}
|
|
920
|
+
return [];
|
|
921
|
+
case "edit": {
|
|
922
|
+
const was = passing.has(key);
|
|
923
|
+
const now = this.#predicate(change.node);
|
|
924
|
+
if (was && now) return [change];
|
|
925
|
+
if (was && !now) {
|
|
926
|
+
passing.delete(key);
|
|
927
|
+
return [{ type: "remove", node: change.oldNode }];
|
|
928
|
+
}
|
|
929
|
+
if (!was && now) {
|
|
930
|
+
passing.add(key);
|
|
931
|
+
return [{ type: "add", node: change.node }];
|
|
932
|
+
}
|
|
933
|
+
return [];
|
|
934
|
+
}
|
|
935
|
+
case "child": {
|
|
936
|
+
const was = passing.has(key);
|
|
937
|
+
const now = this.#predicate(change.node);
|
|
938
|
+
if (was && now) return [change];
|
|
939
|
+
if (was && !now) {
|
|
940
|
+
passing.delete(key);
|
|
941
|
+
return [{ type: "remove", node: change.node }];
|
|
942
|
+
}
|
|
943
|
+
if (!was && now) {
|
|
944
|
+
passing.add(key);
|
|
945
|
+
return [{ type: "add", node: change.node }];
|
|
946
|
+
}
|
|
947
|
+
return [];
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
setOutput(o) {
|
|
952
|
+
this.output = o;
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
var Take = class {
|
|
956
|
+
output = null;
|
|
957
|
+
#input;
|
|
958
|
+
#limit;
|
|
959
|
+
#pk;
|
|
960
|
+
#partitionKey;
|
|
961
|
+
#cmp;
|
|
962
|
+
/** All rows per partition, kept in sort order (incrementally maintained). */
|
|
963
|
+
#partitions = null;
|
|
964
|
+
/** The last-emitted top-`limit` window per partition (for diffing). */
|
|
965
|
+
#windows = /* @__PURE__ */ new Map();
|
|
966
|
+
constructor(input, limit, pk, partitionKey, cmp) {
|
|
967
|
+
this.#input = input;
|
|
968
|
+
this.#limit = limit;
|
|
969
|
+
this.#pk = pk;
|
|
970
|
+
this.#partitionKey = partitionKey;
|
|
971
|
+
this.#cmp = cmp;
|
|
972
|
+
input.setOutput(this);
|
|
973
|
+
}
|
|
974
|
+
#partition(row) {
|
|
975
|
+
return this.#partitionKey ? JSON.stringify(this.#partitionKey.map((k) => row[k] ?? null)) : "";
|
|
976
|
+
}
|
|
977
|
+
#ensure() {
|
|
978
|
+
if (!this.#partitions) {
|
|
979
|
+
this.#partitions = /* @__PURE__ */ new Map();
|
|
980
|
+
for (const n of this.#input.fetch({})) {
|
|
981
|
+
const p = this.#partition(n.row);
|
|
982
|
+
const arr = this.#partitions.get(p) ?? [];
|
|
983
|
+
arr.push(n);
|
|
984
|
+
this.#partitions.set(p, arr);
|
|
985
|
+
}
|
|
986
|
+
for (const [p, arr] of this.#partitions) this.#windows.set(p, arr.slice(0, this.#limit));
|
|
987
|
+
}
|
|
988
|
+
return this.#partitions;
|
|
989
|
+
}
|
|
990
|
+
fetch(req) {
|
|
991
|
+
this.#ensure();
|
|
992
|
+
if (req.constraint || !this.#partitionKey) {
|
|
993
|
+
return this.#input.fetch(req).slice(0, this.#limit);
|
|
994
|
+
}
|
|
995
|
+
const out = [];
|
|
996
|
+
for (const arr of this.#partitions.values()) out.push(...arr.slice(0, this.#limit));
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
/** Incremental: touch only the affected partition(s) and diff their windows. */
|
|
1000
|
+
push(change) {
|
|
1001
|
+
this.#ensure();
|
|
1002
|
+
const out = [];
|
|
1003
|
+
switch (change.type) {
|
|
1004
|
+
case "add":
|
|
1005
|
+
this.#updatePartition(this.#partition(change.node.row), (arr) => insertSorted(arr, change.node, this.#cmp), out);
|
|
1006
|
+
break;
|
|
1007
|
+
case "remove":
|
|
1008
|
+
this.#updatePartition(this.#partition(change.node.row), (arr) => removeByPk(arr, change.node.row, this.#pk), out);
|
|
1009
|
+
break;
|
|
1010
|
+
case "child":
|
|
1011
|
+
this.#updatePartition(this.#partition(change.node.row), (arr) => replaceByPk(arr, change.node, this.#pk), out);
|
|
1012
|
+
break;
|
|
1013
|
+
case "edit": {
|
|
1014
|
+
const po = this.#partition(change.oldNode.row);
|
|
1015
|
+
const pn = this.#partition(change.node.row);
|
|
1016
|
+
if (po === pn) {
|
|
1017
|
+
this.#updatePartition(po, (arr) => {
|
|
1018
|
+
removeByPk(arr, change.oldNode.row, this.#pk);
|
|
1019
|
+
insertSorted(arr, change.node, this.#cmp);
|
|
1020
|
+
}, out);
|
|
1021
|
+
} else {
|
|
1022
|
+
this.#updatePartition(po, (arr) => removeByPk(arr, change.oldNode.row, this.#pk), out);
|
|
1023
|
+
this.#updatePartition(pn, (arr) => insertSorted(arr, change.node, this.#cmp), out);
|
|
1024
|
+
}
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|
|
1030
|
+
#updatePartition(p, mutate, out) {
|
|
1031
|
+
const arr = this.#partitions.get(p) ?? [];
|
|
1032
|
+
mutate(arr);
|
|
1033
|
+
this.#partitions.set(p, arr);
|
|
1034
|
+
const newWin = arr.slice(0, this.#limit);
|
|
1035
|
+
const oldWin = this.#windows.get(p) ?? [];
|
|
1036
|
+
diffWindow(oldWin, newWin, this.#pk, out);
|
|
1037
|
+
this.#windows.set(p, newWin);
|
|
1038
|
+
}
|
|
1039
|
+
setOutput(o) {
|
|
1040
|
+
this.output = o;
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
function insertSorted(arr, node, cmp) {
|
|
1044
|
+
let lo = 0;
|
|
1045
|
+
let hi = arr.length;
|
|
1046
|
+
while (lo < hi) {
|
|
1047
|
+
const mid = lo + hi >> 1;
|
|
1048
|
+
if (cmp(arr[mid].row, node.row) < 0) lo = mid + 1;
|
|
1049
|
+
else hi = mid;
|
|
1050
|
+
}
|
|
1051
|
+
arr.splice(lo, 0, node);
|
|
1052
|
+
}
|
|
1053
|
+
function removeByPk(arr, row, pk) {
|
|
1054
|
+
const k = pkKey(pk, row);
|
|
1055
|
+
const i = arr.findIndex((n) => pkKey(pk, n.row) === k);
|
|
1056
|
+
if (i >= 0) arr.splice(i, 1);
|
|
1057
|
+
}
|
|
1058
|
+
function replaceByPk(arr, node, pk) {
|
|
1059
|
+
const k = pkKey(pk, node.row);
|
|
1060
|
+
const i = arr.findIndex((n) => pkKey(pk, n.row) === k);
|
|
1061
|
+
if (i >= 0) arr[i] = node;
|
|
1062
|
+
}
|
|
1063
|
+
function diffWindow(oldWin, newWin, pk, out) {
|
|
1064
|
+
const oldMap = new Map(oldWin.map((n) => [pkKey(pk, n.row), n]));
|
|
1065
|
+
const newMap = new Map(newWin.map((n) => [pkKey(pk, n.row), n]));
|
|
1066
|
+
for (const [k, o] of oldMap) {
|
|
1067
|
+
const n = newMap.get(k);
|
|
1068
|
+
if (!n || !nodeEq(o, n)) out.push({ type: "remove", node: o });
|
|
1069
|
+
}
|
|
1070
|
+
for (const [k, n] of newMap) {
|
|
1071
|
+
const o = oldMap.get(k);
|
|
1072
|
+
if (!o || !nodeEq(o, n)) out.push({ type: "add", node: n });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
var Join = class {
|
|
1076
|
+
output = null;
|
|
1077
|
+
#parent;
|
|
1078
|
+
#child;
|
|
1079
|
+
#parentKey;
|
|
1080
|
+
#childKey;
|
|
1081
|
+
#rel;
|
|
1082
|
+
constructor(parent, child, parentKey, childKey, relationshipName2) {
|
|
1083
|
+
this.#parent = parent;
|
|
1084
|
+
this.#child = child;
|
|
1085
|
+
this.#parentKey = parentKey;
|
|
1086
|
+
this.#childKey = childKey;
|
|
1087
|
+
this.#rel = relationshipName2;
|
|
1088
|
+
parent.setOutput(new JoinParentPort(this));
|
|
1089
|
+
child.setOutput(new JoinChildPort(this));
|
|
1090
|
+
}
|
|
1091
|
+
#process(parent) {
|
|
1092
|
+
const constraint = buildJoinConstraint(parent.row, this.#parentKey, this.#childKey);
|
|
1093
|
+
const children = constraint ? this.#child.fetch({ constraint }) : [];
|
|
1094
|
+
return { row: parent.row, relationships: { ...parent.relationships, [this.#rel]: children } };
|
|
1095
|
+
}
|
|
1096
|
+
fetch(req) {
|
|
1097
|
+
return this.#parent.fetch(req).map((n) => this.#process(n));
|
|
1098
|
+
}
|
|
1099
|
+
push() {
|
|
1100
|
+
throw new Error("Join is pushed via its parent/child ports");
|
|
1101
|
+
}
|
|
1102
|
+
setOutput(o) {
|
|
1103
|
+
this.output = o;
|
|
1104
|
+
}
|
|
1105
|
+
pushParent(change) {
|
|
1106
|
+
switch (change.type) {
|
|
1107
|
+
case "add":
|
|
1108
|
+
return [{ type: "add", node: this.#process(change.node) }];
|
|
1109
|
+
case "remove":
|
|
1110
|
+
return [{ type: "remove", node: this.#process(change.node) }];
|
|
1111
|
+
case "child":
|
|
1112
|
+
return [{
|
|
1113
|
+
type: "child",
|
|
1114
|
+
node: this.#process(change.node),
|
|
1115
|
+
relationshipName: change.relationshipName,
|
|
1116
|
+
change: change.change
|
|
1117
|
+
}];
|
|
1118
|
+
case "edit":
|
|
1119
|
+
if (sameKey(change.oldNode.row, change.node.row, this.#parentKey)) {
|
|
1120
|
+
return [{ type: "edit", node: this.#process(change.node), oldNode: this.#process(change.oldNode) }];
|
|
1121
|
+
}
|
|
1122
|
+
return [
|
|
1123
|
+
{ type: "remove", node: this.#process(change.oldNode) },
|
|
1124
|
+
{ type: "add", node: this.#process(change.node) }
|
|
1125
|
+
];
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
pushChild(change) {
|
|
1129
|
+
const childRows = [change.node.row];
|
|
1130
|
+
if (change.type === "edit" && !sameKey(change.oldNode.row, change.node.row, this.#childKey)) {
|
|
1131
|
+
childRows[0] = change.node.row;
|
|
1132
|
+
childRows.push(change.oldNode.row);
|
|
1133
|
+
}
|
|
1134
|
+
const out = [];
|
|
1135
|
+
for (const childRow of childRows) {
|
|
1136
|
+
const constraint = buildJoinConstraint(childRow, this.#childKey, this.#parentKey);
|
|
1137
|
+
if (!constraint) continue;
|
|
1138
|
+
for (const p of this.#parent.fetch({ constraint })) {
|
|
1139
|
+
out.push({ type: "child", node: this.#process(p), relationshipName: this.#rel, change });
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return out;
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
function sameKey(a, b, keys) {
|
|
1146
|
+
return keys.every((k) => a[k] === b[k]);
|
|
1147
|
+
}
|
|
1148
|
+
var JoinParentPort = class {
|
|
1149
|
+
#join;
|
|
1150
|
+
constructor(join) {
|
|
1151
|
+
this.#join = join;
|
|
1152
|
+
}
|
|
1153
|
+
get output() {
|
|
1154
|
+
return this.#join.output;
|
|
1155
|
+
}
|
|
1156
|
+
set output(_o) {
|
|
1157
|
+
}
|
|
1158
|
+
fetch() {
|
|
1159
|
+
throw new Error("join port has no fetch");
|
|
1160
|
+
}
|
|
1161
|
+
push(c) {
|
|
1162
|
+
return this.#join.pushParent(c);
|
|
1163
|
+
}
|
|
1164
|
+
setOutput() {
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
var JoinChildPort = class {
|
|
1168
|
+
#join;
|
|
1169
|
+
constructor(join) {
|
|
1170
|
+
this.#join = join;
|
|
1171
|
+
}
|
|
1172
|
+
get output() {
|
|
1173
|
+
return this.#join.output;
|
|
1174
|
+
}
|
|
1175
|
+
set output(_o) {
|
|
1176
|
+
}
|
|
1177
|
+
fetch() {
|
|
1178
|
+
throw new Error("join port has no fetch");
|
|
1179
|
+
}
|
|
1180
|
+
push(c) {
|
|
1181
|
+
return this.#join.pushChild(c);
|
|
1182
|
+
}
|
|
1183
|
+
setOutput() {
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
// client/src/ivm/build.ts
|
|
1188
|
+
var SUBQ = "zsubq_";
|
|
1189
|
+
function buildPipeline(ast, provider, partitionKey = null) {
|
|
1190
|
+
const pk = provider.pkOf(ast.table);
|
|
1191
|
+
const order = completeOrder2(ast.orderBy, pk);
|
|
1192
|
+
let current = provider.connect(ast.table, order);
|
|
1193
|
+
if (ast.start) current = skip(current, ast.start.row, ast.start.exclusive, makeComparator(order));
|
|
1194
|
+
if (ast.where) {
|
|
1195
|
+
if (conditionHasExists(ast.where)) {
|
|
1196
|
+
const joins = [];
|
|
1197
|
+
const counter = { n: 0 };
|
|
1198
|
+
const resolved = resolveCondition(ast.where, joins, counter);
|
|
1199
|
+
for (const { relName, related } of joins) {
|
|
1200
|
+
const child = buildPipeline(related.subquery, provider);
|
|
1201
|
+
current = new Join(
|
|
1202
|
+
current,
|
|
1203
|
+
child,
|
|
1204
|
+
related.correlation.parentField,
|
|
1205
|
+
related.correlation.childField,
|
|
1206
|
+
relName
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
current = new CondFilter(current, nodePredicate(resolved), pk);
|
|
1210
|
+
} else {
|
|
1211
|
+
current = new Filter(current, rowPredicate(ast.where));
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
for (const sub of ast.related ?? []) {
|
|
1215
|
+
const child = buildPipeline(sub.subquery, provider, sub.correlation.childField);
|
|
1216
|
+
current = new Join(
|
|
1217
|
+
current,
|
|
1218
|
+
child,
|
|
1219
|
+
sub.correlation.parentField,
|
|
1220
|
+
sub.correlation.childField,
|
|
1221
|
+
relationshipName(sub)
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
if (ast.limit != null) current = new Take(current, ast.limit, pk, partitionKey, makeComparator(order));
|
|
1225
|
+
return current;
|
|
1226
|
+
}
|
|
1227
|
+
function relationshipName(sub) {
|
|
1228
|
+
const a = sub.subquery.alias;
|
|
1229
|
+
if (a) return a.startsWith(SUBQ) ? a.slice(SUBQ.length) : a;
|
|
1230
|
+
return sub.subquery.table;
|
|
1231
|
+
}
|
|
1232
|
+
function resolveCondition(cond, joins, counter) {
|
|
1233
|
+
switch (cond.type) {
|
|
1234
|
+
case "simple":
|
|
1235
|
+
return { kind: "simple", cond };
|
|
1236
|
+
case "and":
|
|
1237
|
+
return { kind: "and", items: cond.conditions.map((c) => resolveCondition(c, joins, counter)) };
|
|
1238
|
+
case "or":
|
|
1239
|
+
return { kind: "or", items: cond.conditions.map((c) => resolveCondition(c, joins, counter)) };
|
|
1240
|
+
case "correlatedSubquery": {
|
|
1241
|
+
const relName = cond.related.subquery.alias ?? `${SUBQ}exists_${counter.n++}`;
|
|
1242
|
+
joins.push({ relName, related: cond.related });
|
|
1243
|
+
return { kind: "exists", relName, negated: cond.op === "NOT EXISTS" };
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function nodePredicate(resolved) {
|
|
1248
|
+
const evalResolved = (r, node) => {
|
|
1249
|
+
switch (r.kind) {
|
|
1250
|
+
case "simple":
|
|
1251
|
+
return simpleMatches(node.row, r.cond);
|
|
1252
|
+
case "and":
|
|
1253
|
+
return r.items.every((x) => evalResolved(x, node));
|
|
1254
|
+
case "or":
|
|
1255
|
+
return r.items.some((x) => evalResolved(x, node));
|
|
1256
|
+
case "exists": {
|
|
1257
|
+
const present = (node.relationships[r.relName]?.length ?? 0) > 0;
|
|
1258
|
+
return present !== r.negated;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
return (node) => evalResolved(resolved, node);
|
|
1263
|
+
}
|
|
1264
|
+
function rowPredicate(cond) {
|
|
1265
|
+
const evalRow = (c, row) => {
|
|
1266
|
+
switch (c.type) {
|
|
1267
|
+
case "simple":
|
|
1268
|
+
return simpleMatches(row, c);
|
|
1269
|
+
case "and":
|
|
1270
|
+
return c.conditions.every((x) => evalRow(x, row));
|
|
1271
|
+
case "or":
|
|
1272
|
+
return c.conditions.some((x) => evalRow(x, row));
|
|
1273
|
+
case "correlatedSubquery":
|
|
1274
|
+
throw new Error("EXISTS must be handled by CondFilter");
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
return (row) => evalRow(cond, row);
|
|
1278
|
+
}
|
|
1279
|
+
function conditionHasExists(cond) {
|
|
1280
|
+
switch (cond.type) {
|
|
1281
|
+
case "correlatedSubquery":
|
|
1282
|
+
return true;
|
|
1283
|
+
case "and":
|
|
1284
|
+
case "or":
|
|
1285
|
+
return cond.conditions.some(conditionHasExists);
|
|
1286
|
+
case "simple":
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// client/src/ivm/view.ts
|
|
1292
|
+
var MaterializedView = class {
|
|
1293
|
+
/** The current top-level result, in sort order. */
|
|
1294
|
+
nodes;
|
|
1295
|
+
#cmp;
|
|
1296
|
+
#pk;
|
|
1297
|
+
constructor(top, order, pk) {
|
|
1298
|
+
this.#cmp = makeComparator(order);
|
|
1299
|
+
this.#pk = pk;
|
|
1300
|
+
this.nodes = top.fetch({});
|
|
1301
|
+
const terminal = {
|
|
1302
|
+
output: null,
|
|
1303
|
+
fetch: () => [],
|
|
1304
|
+
setOutput: () => {
|
|
1305
|
+
},
|
|
1306
|
+
push: (c) => {
|
|
1307
|
+
this.#apply(c);
|
|
1308
|
+
return [];
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
top.setOutput(terminal);
|
|
1312
|
+
}
|
|
1313
|
+
#apply(change) {
|
|
1314
|
+
switch (change.type) {
|
|
1315
|
+
case "add":
|
|
1316
|
+
this.#insert(change.node);
|
|
1317
|
+
break;
|
|
1318
|
+
case "remove":
|
|
1319
|
+
this.#removeByPk(change.node.row);
|
|
1320
|
+
break;
|
|
1321
|
+
case "edit":
|
|
1322
|
+
this.#removeByPk(change.oldNode.row);
|
|
1323
|
+
this.#insert(change.node);
|
|
1324
|
+
break;
|
|
1325
|
+
case "child": {
|
|
1326
|
+
const i = this.#indexByPk(change.node.row);
|
|
1327
|
+
if (i >= 0) this.nodes[i] = change.node;
|
|
1328
|
+
break;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
#insert(node) {
|
|
1333
|
+
let lo = 0;
|
|
1334
|
+
let hi = this.nodes.length;
|
|
1335
|
+
while (lo < hi) {
|
|
1336
|
+
const mid = lo + hi >> 1;
|
|
1337
|
+
if (this.#cmp(this.nodes[mid].row, node.row) < 0) lo = mid + 1;
|
|
1338
|
+
else hi = mid;
|
|
1339
|
+
}
|
|
1340
|
+
this.nodes.splice(lo, 0, node);
|
|
1341
|
+
}
|
|
1342
|
+
#indexByPk(row) {
|
|
1343
|
+
const k = pkKey(this.#pk, row);
|
|
1344
|
+
return this.nodes.findIndex((n) => pkKey(this.#pk, n.row) === k);
|
|
1345
|
+
}
|
|
1346
|
+
#removeByPk(row) {
|
|
1347
|
+
const i = this.#indexByPk(row);
|
|
1348
|
+
if (i >= 0) this.nodes.splice(i, 1);
|
|
1349
|
+
}
|
|
1350
|
+
/** The result as Zero's `{row, rels}` snapshot shape (hidden `zsubq_*` excluded). */
|
|
1351
|
+
snapshot() {
|
|
1352
|
+
return this.nodes.map(normNode);
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
function normNode(node) {
|
|
1356
|
+
const rels = {};
|
|
1357
|
+
for (const k of Object.keys(node.relationships).sort()) {
|
|
1358
|
+
if (k.startsWith("zsubq_")) continue;
|
|
1359
|
+
rels[k] = node.relationships[k].map(normNode);
|
|
1360
|
+
}
|
|
1361
|
+
return { row: node.row, rels };
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// client/src/ivm/source.ts
|
|
1365
|
+
var MemorySource = class {
|
|
1366
|
+
table;
|
|
1367
|
+
pk;
|
|
1368
|
+
#rows = /* @__PURE__ */ new Map();
|
|
1369
|
+
#connections = [];
|
|
1370
|
+
constructor(table2, pk) {
|
|
1371
|
+
this.table = table2;
|
|
1372
|
+
this.pk = pk;
|
|
1373
|
+
}
|
|
1374
|
+
insertInitial(row) {
|
|
1375
|
+
this.#rows.set(pkKey(this.pk, row), row);
|
|
1376
|
+
}
|
|
1377
|
+
rows() {
|
|
1378
|
+
return [...this.#rows.values()];
|
|
1379
|
+
}
|
|
1380
|
+
get(key) {
|
|
1381
|
+
return this.#rows.get(key);
|
|
1382
|
+
}
|
|
1383
|
+
connect(order) {
|
|
1384
|
+
const conn = { output: null, order, cmp: makeComparator(order) };
|
|
1385
|
+
this.#connections.push(conn);
|
|
1386
|
+
return new SourceConnection(this, conn);
|
|
1387
|
+
}
|
|
1388
|
+
fetchConn(req, conn) {
|
|
1389
|
+
let rows = [...this.#rows.values()];
|
|
1390
|
+
if (req.constraint) rows = rows.filter((r) => constraintMatches(req.constraint, r));
|
|
1391
|
+
const cmp = req.reverse ? makeComparator(conn.order, true) : conn.cmp;
|
|
1392
|
+
rows = [...rows].sort(cmp);
|
|
1393
|
+
if (req.start) {
|
|
1394
|
+
const { row, basis } = req.start;
|
|
1395
|
+
const i = rows.findIndex((r) => {
|
|
1396
|
+
const c = cmp(r, row);
|
|
1397
|
+
return basis === "after" ? c > 0 : c >= 0;
|
|
1398
|
+
});
|
|
1399
|
+
rows = i < 0 ? [] : rows.slice(i);
|
|
1400
|
+
}
|
|
1401
|
+
return rows.map((r) => ({ row: r, relationships: {} }));
|
|
1402
|
+
}
|
|
1403
|
+
/** Apply a source change (commit-first) and propagate to all connections. */
|
|
1404
|
+
push(change) {
|
|
1405
|
+
if (change.type === "add") {
|
|
1406
|
+
this.#rows.set(pkKey(this.pk, change.row), change.row);
|
|
1407
|
+
} else if (change.type === "remove") {
|
|
1408
|
+
this.#rows.delete(pkKey(this.pk, change.row));
|
|
1409
|
+
} else {
|
|
1410
|
+
this.#rows.delete(pkKey(this.pk, change.oldRow));
|
|
1411
|
+
this.#rows.set(pkKey(this.pk, change.row), change.row);
|
|
1412
|
+
}
|
|
1413
|
+
for (const conn of this.#connections) {
|
|
1414
|
+
if (conn.output) deliver(conn.output, toChange(change));
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
var MemorySourceProvider = class {
|
|
1419
|
+
#sources = /* @__PURE__ */ new Map();
|
|
1420
|
+
add(table2, pk, rows) {
|
|
1421
|
+
const s = new MemorySource(table2, pk);
|
|
1422
|
+
for (const r of rows) s.insertInitial(r);
|
|
1423
|
+
this.#sources.set(table2, s);
|
|
1424
|
+
return s;
|
|
1425
|
+
}
|
|
1426
|
+
source(table2) {
|
|
1427
|
+
const s = this.#sources.get(table2);
|
|
1428
|
+
if (!s) throw new Error(`no source for table ${table2}`);
|
|
1429
|
+
return s;
|
|
1430
|
+
}
|
|
1431
|
+
pkOf(table2) {
|
|
1432
|
+
return this.source(table2).pk;
|
|
1433
|
+
}
|
|
1434
|
+
connect(table2, order) {
|
|
1435
|
+
return this.source(table2).connect(order);
|
|
1436
|
+
}
|
|
1437
|
+
push(table2, change) {
|
|
1438
|
+
this.source(table2).push(change);
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
function toChange(change) {
|
|
1442
|
+
if (change.type === "add") return { type: "add", node: { row: change.row, relationships: {} } };
|
|
1443
|
+
if (change.type === "remove") return { type: "remove", node: { row: change.row, relationships: {} } };
|
|
1444
|
+
return {
|
|
1445
|
+
type: "edit",
|
|
1446
|
+
node: { row: change.row, relationships: {} },
|
|
1447
|
+
oldNode: { row: change.oldRow, relationships: {} }
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
var SourceConnection = class {
|
|
1451
|
+
output = null;
|
|
1452
|
+
#src;
|
|
1453
|
+
#conn;
|
|
1454
|
+
constructor(src, conn) {
|
|
1455
|
+
this.#src = src;
|
|
1456
|
+
this.#conn = conn;
|
|
1457
|
+
}
|
|
1458
|
+
fetch(req) {
|
|
1459
|
+
return this.#src.fetchConn(req, this.#conn);
|
|
1460
|
+
}
|
|
1461
|
+
push() {
|
|
1462
|
+
throw new Error("a source connection never receives an upstream push");
|
|
1463
|
+
}
|
|
1464
|
+
setOutput(o) {
|
|
1465
|
+
this.output = o;
|
|
1466
|
+
this.#conn.output = o;
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
// client/src/ivm/store-provider.ts
|
|
1471
|
+
var StoreProvider = class {
|
|
1472
|
+
#store;
|
|
1473
|
+
#sources = /* @__PURE__ */ new Map();
|
|
1474
|
+
constructor(store) {
|
|
1475
|
+
this.#store = store;
|
|
1476
|
+
}
|
|
1477
|
+
pkOf(table2) {
|
|
1478
|
+
return this.#store.pkOf(table2);
|
|
1479
|
+
}
|
|
1480
|
+
connect(table2, order) {
|
|
1481
|
+
return this.#ensure(table2).connect(order);
|
|
1482
|
+
}
|
|
1483
|
+
#ensure(table2) {
|
|
1484
|
+
let s = this.#sources.get(table2);
|
|
1485
|
+
if (!s) {
|
|
1486
|
+
s = new MemorySource(table2, this.#store.pkOf(table2));
|
|
1487
|
+
for (const row of this.#store.effectiveRows(table2)) s.insertInitial(row);
|
|
1488
|
+
this.#sources.set(table2, s);
|
|
1489
|
+
}
|
|
1490
|
+
return s;
|
|
1491
|
+
}
|
|
1492
|
+
/** Push the change for one touched effective-row key into its table's source. */
|
|
1493
|
+
applyChange(table2, key) {
|
|
1494
|
+
const src = this.#sources.get(table2);
|
|
1495
|
+
if (!src) return;
|
|
1496
|
+
const next = this.#store.effectiveRow(table2, key);
|
|
1497
|
+
const prev = src.get(key);
|
|
1498
|
+
if (prev && next) {
|
|
1499
|
+
if (!rowEq(prev, next)) src.push({ type: "edit", row: next, oldRow: prev });
|
|
1500
|
+
} else if (next) {
|
|
1501
|
+
src.push({ type: "add", row: next });
|
|
1502
|
+
} else if (prev) {
|
|
1503
|
+
src.push({ type: "remove", row: prev });
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
function tablesOf(ast, out = /* @__PURE__ */ new Set()) {
|
|
1508
|
+
out.add(ast.table);
|
|
1509
|
+
for (const rel of ast.related ?? []) tablesOf(rel.subquery, out);
|
|
1510
|
+
for (const rel of existsRelationships(ast.where)) tablesOf(rel.subquery, out);
|
|
1511
|
+
collectExistsTables(ast.where, out);
|
|
1512
|
+
return out;
|
|
1513
|
+
}
|
|
1514
|
+
function collectExistsTables(cond, out) {
|
|
1515
|
+
if (!cond) return;
|
|
1516
|
+
if (cond.type === "correlatedSubquery") tablesOf(cond.related.subquery, out);
|
|
1517
|
+
else if (cond.type === "and" || cond.type === "or") for (const c of cond.conditions) collectExistsTables(c, out);
|
|
1518
|
+
}
|
|
1519
|
+
function nodeToRow(node, ast) {
|
|
1520
|
+
const out = { ...node.row };
|
|
1521
|
+
for (const rel of ast.related ?? []) {
|
|
1522
|
+
const alias = rel.subquery.alias ?? rel.subquery.table;
|
|
1523
|
+
if (rel.hidden) {
|
|
1524
|
+
const inner = rel.subquery.related[0];
|
|
1525
|
+
const innerAlias = inner.subquery.alias ?? inner.subquery.table;
|
|
1526
|
+
const junctionNodes = node.relationships[alias] ?? [];
|
|
1527
|
+
const dest = junctionNodes.flatMap((jn) => (jn.relationships[innerAlias] ?? []).map((c) => nodeToRow(c, inner.subquery)));
|
|
1528
|
+
out[alias] = inner.singular ? dest[0] : dest;
|
|
1529
|
+
} else {
|
|
1530
|
+
const children = (node.relationships[alias] ?? []).map((c) => nodeToRow(c, rel.subquery));
|
|
1531
|
+
out[alias] = rel.singular ? children[0] : children;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
for (const rel of existsRelationships(ast.where)) {
|
|
1535
|
+
const alias = rel.subquery.alias;
|
|
1536
|
+
out[alias] = (node.relationships[alias] ?? []).map((c) => nodeToRow(c, rel.subquery));
|
|
1537
|
+
}
|
|
1538
|
+
return out;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// client/src/client.ts
|
|
1542
|
+
function encodeSecProtocol(authToken) {
|
|
1543
|
+
const payload = JSON.stringify({ initConnectionMessage: null, authToken });
|
|
1544
|
+
const b64 = typeof btoa === "function" ? btoa(payload) : Buffer.from(payload, "utf8").toString("base64");
|
|
1545
|
+
return encodeURIComponent(b64);
|
|
1546
|
+
}
|
|
1547
|
+
var View = class {
|
|
1548
|
+
#ast;
|
|
1549
|
+
#unsub;
|
|
1550
|
+
#onDestroy;
|
|
1551
|
+
#provider;
|
|
1552
|
+
#mview;
|
|
1553
|
+
#tables;
|
|
1554
|
+
data = [];
|
|
1555
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
1556
|
+
constructor(store, ast, applyWhere = true, onDestroy) {
|
|
1557
|
+
this.#ast = applyWhere ? ast : { ...ast, where: void 0 };
|
|
1558
|
+
this.#onDestroy = onDestroy;
|
|
1559
|
+
this.#tables = tablesOf(this.#ast);
|
|
1560
|
+
this.#provider = new StoreProvider(store);
|
|
1561
|
+
const pk = store.pkOf(this.#ast.table);
|
|
1562
|
+
const top = buildPipeline(this.#ast, this.#provider);
|
|
1563
|
+
this.#mview = new MaterializedView(top, completeOrder2(this.#ast.orderBy, pk), pk);
|
|
1564
|
+
this.#refresh();
|
|
1565
|
+
this.#unsub = store.subscribe((changed) => this.#onChange(changed));
|
|
1566
|
+
}
|
|
1567
|
+
#onChange(changed) {
|
|
1568
|
+
let touched = false;
|
|
1569
|
+
for (const { table: table2, key } of changed) {
|
|
1570
|
+
if (this.#tables.has(table2)) {
|
|
1571
|
+
this.#provider.applyChange(table2, key);
|
|
1572
|
+
touched = true;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (touched) this.#refresh();
|
|
1576
|
+
}
|
|
1577
|
+
#refresh() {
|
|
1578
|
+
this.data = this.#mview.nodes.map((n) => nodeToRow(n, this.#ast));
|
|
1579
|
+
for (const fn of this.#listeners) fn();
|
|
1580
|
+
}
|
|
1581
|
+
subscribe(fn) {
|
|
1582
|
+
this.#listeners.add(fn);
|
|
1583
|
+
return () => this.#listeners.delete(fn);
|
|
1584
|
+
}
|
|
1585
|
+
/** Stop reacting to store changes + release the query subscription (Zero's `view.destroy()`). */
|
|
1586
|
+
destroy() {
|
|
1587
|
+
this.#unsub();
|
|
1588
|
+
this.#listeners.clear();
|
|
1589
|
+
this.#onDestroy?.();
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
var Orbit = class {
|
|
1593
|
+
// Not `readonly`: when `persist` is enabled and no explicit id was given, these
|
|
1594
|
+
// are restored from the KV in `#init` so a reload keeps the same identity (and
|
|
1595
|
+
// thus resumes the same server CVR as a fast delta instead of a full resync).
|
|
1596
|
+
clientID;
|
|
1597
|
+
clientGroupID;
|
|
1598
|
+
/** Per-table typed query builder (ad-hoc queries): `orbit.query.todo.where(...)`. */
|
|
1599
|
+
query;
|
|
1600
|
+
/** Custom (named) queries from the `queries` option: `orbit.queries.allTodos()`. */
|
|
1601
|
+
queries;
|
|
1602
|
+
/**
|
|
1603
|
+
* Mutators. With a `mutators` option these are your custom mutators
|
|
1604
|
+
* (`orbit.mutate.createTodo(args)`); otherwise per-table CRUD
|
|
1605
|
+
* (`orbit.mutate.todo.insert(...)`).
|
|
1606
|
+
*/
|
|
1607
|
+
mutate;
|
|
1608
|
+
#ws;
|
|
1609
|
+
#store;
|
|
1610
|
+
#opts;
|
|
1611
|
+
#pkByTable;
|
|
1612
|
+
#schema;
|
|
1613
|
+
#mutatorDefs;
|
|
1614
|
+
#nextMutationID = 1;
|
|
1615
|
+
#closed = false;
|
|
1616
|
+
#connecting = false;
|
|
1617
|
+
#reconnectMs = 500;
|
|
1618
|
+
#maxReconnectMs;
|
|
1619
|
+
#queryTTL;
|
|
1620
|
+
#kv;
|
|
1621
|
+
/** Last server cookie we've fully applied; sent as `baseCookie` on reconnect so
|
|
1622
|
+
* the server can prove a delta resume is safe (else it full-resyncs). */
|
|
1623
|
+
#cookie;
|
|
1624
|
+
/** Query lifetime: dedup + TTL + GC. Its `active()` set is resent on reconnect. */
|
|
1625
|
+
#queries;
|
|
1626
|
+
/** Unconfirmed mutations by id — resent on (re)connect, dropped on confirm. */
|
|
1627
|
+
#unconfirmedPushes = /* @__PURE__ */ new Map();
|
|
1628
|
+
/** In-flight poke buffer: rows + lastMutationID changes accumulate across
|
|
1629
|
+
* `pokePart`s and are applied atomically on `pokeEnd`. A mid-poke disconnect (or
|
|
1630
|
+
* `pokeEnd.cancel`) discards it, so the store never holds a torn partial poke. */
|
|
1631
|
+
#poke = null;
|
|
1632
|
+
/** Surfaced when the server sends a terminal `error` message. */
|
|
1633
|
+
#onError;
|
|
1634
|
+
/** Whether the id was supplied explicitly (then we never override it from the KV). */
|
|
1635
|
+
#idFromOpts;
|
|
1636
|
+
constructor(opts) {
|
|
1637
|
+
this.#opts = opts;
|
|
1638
|
+
this.#schema = opts.schema;
|
|
1639
|
+
this.#maxReconnectMs = opts.maxReconnectMs ?? 3e4;
|
|
1640
|
+
this.#queryTTL = opts.queryTTL ?? "5m";
|
|
1641
|
+
this.#kv = opts.persist;
|
|
1642
|
+
this.#onError = opts.onError;
|
|
1643
|
+
this.#queries = new QueryManager({
|
|
1644
|
+
onSubscribe: (put) => this.#send(["changeDesiredQueries", { desiredQueriesPatch: [put] }]),
|
|
1645
|
+
onUnsubscribe: (hash) => this.#send(["changeDesiredQueries", { desiredQueriesPatch: [{ op: "del", hash }] }])
|
|
1646
|
+
});
|
|
1647
|
+
this.#idFromOpts = opts.clientID != null;
|
|
1648
|
+
this.clientID = opts.clientID ?? Math.random().toString(36).slice(2);
|
|
1649
|
+
this.clientGroupID = opts.clientGroupID ?? this.clientID;
|
|
1650
|
+
this.#pkByTable = {};
|
|
1651
|
+
for (const t of Object.values(opts.schema?.tables ?? {})) {
|
|
1652
|
+
this.#pkByTable[t.name] = [...t.primaryKey];
|
|
1653
|
+
}
|
|
1654
|
+
this.#store = new Store(this.#pkByTable);
|
|
1655
|
+
const schemaForQueries = this.#schema ?? { tables: {}, relationships: {} };
|
|
1656
|
+
this.query = new Proxy({}, {
|
|
1657
|
+
get: (_t, table2) => new SchemaQuery(this, schemaForQueries, table2, Query.from(table2))
|
|
1658
|
+
});
|
|
1659
|
+
const queryDefs = opts.queries;
|
|
1660
|
+
this.queries = queryDefs ? new Proxy({}, {
|
|
1661
|
+
get: (_t, name) => (args) => ({
|
|
1662
|
+
materialize: () => {
|
|
1663
|
+
const ast = queryDefs[name]({ args, ctx: {} }).ast();
|
|
1664
|
+
const argList = args === void 0 ? [] : [args];
|
|
1665
|
+
return this.materializeNamed(name, argList, ast);
|
|
1666
|
+
}
|
|
1667
|
+
})
|
|
1668
|
+
}) : {};
|
|
1669
|
+
this.#mutatorDefs = opts.mutators;
|
|
1670
|
+
this.mutate = this.#mutatorDefs ? new Proxy({}, {
|
|
1671
|
+
get: (_t, name) => (args) => this.mutateCustom(name, args)
|
|
1672
|
+
}) : new Proxy({}, {
|
|
1673
|
+
get: (_t, table2) => {
|
|
1674
|
+
const pk = this.#pkByTable[table2] ?? ["id"];
|
|
1675
|
+
const crud = (op, value) => this.mutateCrud({ op, tableName: table2, primaryKey: pk, value });
|
|
1676
|
+
return {
|
|
1677
|
+
insert: (value) => crud("insert", value),
|
|
1678
|
+
upsert: (value) => crud("upsert", value),
|
|
1679
|
+
update: (value) => crud("update", value),
|
|
1680
|
+
delete: (value) => crud("delete", value)
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
void this.#init();
|
|
1685
|
+
}
|
|
1686
|
+
/** Build a raw (untyped) query against `table` — escape hatch. */
|
|
1687
|
+
queryRaw(table2) {
|
|
1688
|
+
return Query.from(table2);
|
|
1689
|
+
}
|
|
1690
|
+
/** Subscribe to an ad-hoc query and return a live [`View`]. */
|
|
1691
|
+
materialize(query, ttl = this.#queryTTL) {
|
|
1692
|
+
const ast = query.ast();
|
|
1693
|
+
const hash = hashAST(ast);
|
|
1694
|
+
const release = this.#queries.add(hash, { op: "put", hash, ast }, ttl);
|
|
1695
|
+
return new View(this.#store, ast, true, release);
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Subscribe to a custom (named) query by name + args. The server's query
|
|
1699
|
+
* endpoint resolves/authorizes it (with the connection's auth) into the actual
|
|
1700
|
+
* query; the client uses the def's `ast` for local ordering/nesting/optimism.
|
|
1701
|
+
*
|
|
1702
|
+
* The local view DOES apply the def's `where` (applyWhere=true): the client
|
|
1703
|
+
* store is shared across all subscriptions, so without it a filtered query like
|
|
1704
|
+
* `issue({id})` would match every row another query synced (e.g. the issues
|
|
1705
|
+
* list) and `.one()` would return the wrong row. This assumes the def's filter
|
|
1706
|
+
* matches the server's resolution, which holds for ordinary parameterized
|
|
1707
|
+
* queries; the store only ever holds server-authorized rows.
|
|
1708
|
+
*/
|
|
1709
|
+
materializeNamed(name, args, ast, ttl = this.#queryTTL) {
|
|
1710
|
+
const hash = hashString(JSON.stringify([name, args]));
|
|
1711
|
+
const release = this.#queries.add(hash, { op: "put", hash, name, args }, ttl);
|
|
1712
|
+
return new View(this.#store, ast, true, release);
|
|
1713
|
+
}
|
|
1714
|
+
/** Apply a single CRUD op (mirrors `z.mutate.<table>.insert(...)`). */
|
|
1715
|
+
mutateCrud(op) {
|
|
1716
|
+
const id = this.#nextMutationID++;
|
|
1717
|
+
const mutation = {
|
|
1718
|
+
type: "crud",
|
|
1719
|
+
id,
|
|
1720
|
+
clientID: this.clientID,
|
|
1721
|
+
name: "_zero_crud",
|
|
1722
|
+
args: [{ ops: [op] }],
|
|
1723
|
+
timestamp: Date.now()
|
|
1724
|
+
};
|
|
1725
|
+
this.#store.addPending(id, [op], mutation);
|
|
1726
|
+
this.#pushMutation(id, mutation);
|
|
1727
|
+
}
|
|
1728
|
+
/** Run a custom mutator by name (optimistically, then on the server). */
|
|
1729
|
+
mutateCustom(name, args) {
|
|
1730
|
+
const id = this.#nextMutationID++;
|
|
1731
|
+
const mutation = {
|
|
1732
|
+
type: "custom",
|
|
1733
|
+
id,
|
|
1734
|
+
clientID: this.clientID,
|
|
1735
|
+
name,
|
|
1736
|
+
args: args === void 0 ? [] : [args],
|
|
1737
|
+
timestamp: Date.now()
|
|
1738
|
+
};
|
|
1739
|
+
const def = this.#mutatorDefs?.[name];
|
|
1740
|
+
let ops = [];
|
|
1741
|
+
if (def && this.#schema) {
|
|
1742
|
+
try {
|
|
1743
|
+
ops = collectOps(this.#schema, def, args, {});
|
|
1744
|
+
} catch {
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
this.#store.addPending(id, ops, mutation);
|
|
1748
|
+
this.#pushMutation(id, mutation);
|
|
1749
|
+
}
|
|
1750
|
+
close() {
|
|
1751
|
+
this.#closed = true;
|
|
1752
|
+
this.#ws?.close();
|
|
1753
|
+
}
|
|
1754
|
+
// --- internals ----------------------------------------------------------
|
|
1755
|
+
/** Hydrate persisted state (if any), restore unconfirmed mutations, then connect. */
|
|
1756
|
+
async #init() {
|
|
1757
|
+
if (this.#kv) {
|
|
1758
|
+
try {
|
|
1759
|
+
await this.#store.hydrate(this.#kv);
|
|
1760
|
+
if (!this.#idFromOpts) {
|
|
1761
|
+
const savedID = await this.#kv.get("clientID");
|
|
1762
|
+
if (typeof savedID === "string") {
|
|
1763
|
+
this.clientID = savedID;
|
|
1764
|
+
const savedGroup = await this.#kv.get("clientGroupID");
|
|
1765
|
+
this.clientGroupID = typeof savedGroup === "string" ? savedGroup : savedID;
|
|
1766
|
+
} else {
|
|
1767
|
+
void this.#kv.set("clientID", this.clientID);
|
|
1768
|
+
void this.#kv.set("clientGroupID", this.clientGroupID);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
let maxId = 0;
|
|
1772
|
+
for (const m of this.#store.pendingMutations()) {
|
|
1773
|
+
maxId = Math.max(maxId, m.id);
|
|
1774
|
+
this.#unconfirmedPushes.set(m.id, ["push", {
|
|
1775
|
+
clientGroupID: this.clientGroupID,
|
|
1776
|
+
mutations: [m],
|
|
1777
|
+
pushVersion: 1,
|
|
1778
|
+
timestamp: m.timestamp,
|
|
1779
|
+
requestID: Math.random().toString(36).slice(2)
|
|
1780
|
+
}]);
|
|
1781
|
+
}
|
|
1782
|
+
if (maxId >= this.#nextMutationID) this.#nextMutationID = maxId + 1;
|
|
1783
|
+
const savedNextID = await this.#kv.get("nextMutationID");
|
|
1784
|
+
if (typeof savedNextID === "number" && savedNextID > this.#nextMutationID) {
|
|
1785
|
+
this.#nextMutationID = savedNextID;
|
|
1786
|
+
}
|
|
1787
|
+
const c = await this.#kv.get("cookie");
|
|
1788
|
+
if (typeof c === "string") this.#cookie = c;
|
|
1789
|
+
} catch {
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
await this.#connect();
|
|
1793
|
+
}
|
|
1794
|
+
#pushMutation(id, mutation) {
|
|
1795
|
+
const msg = ["push", {
|
|
1796
|
+
clientGroupID: this.clientGroupID,
|
|
1797
|
+
mutations: [mutation],
|
|
1798
|
+
pushVersion: 1,
|
|
1799
|
+
timestamp: Date.now(),
|
|
1800
|
+
requestID: Math.random().toString(36).slice(2)
|
|
1801
|
+
}];
|
|
1802
|
+
this.#unconfirmedPushes.set(id, msg);
|
|
1803
|
+
void this.#kv?.set("nextMutationID", this.#nextMutationID);
|
|
1804
|
+
this.#send(msg);
|
|
1805
|
+
}
|
|
1806
|
+
async #connect() {
|
|
1807
|
+
if (this.#closed || this.#connecting) return;
|
|
1808
|
+
if (typeof WebSocket === "undefined") return;
|
|
1809
|
+
this.#connecting = true;
|
|
1810
|
+
const token = typeof this.#opts.auth === "function" ? await this.#opts.auth() : this.#opts.auth;
|
|
1811
|
+
if (this.#closed) {
|
|
1812
|
+
this.#connecting = false;
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
let url = `${this.#opts.server}/sync/v51/connect?clientID=${encodeURIComponent(this.clientID)}`;
|
|
1816
|
+
if (this.#cookie != null) url += `&baseCookie=${encodeURIComponent(this.#cookie)}`;
|
|
1817
|
+
const ws = token ? new WebSocket(url, [encodeSecProtocol(token)]) : new WebSocket(url);
|
|
1818
|
+
this.#ws = ws;
|
|
1819
|
+
ws.addEventListener("open", () => {
|
|
1820
|
+
this.#connecting = false;
|
|
1821
|
+
this.#resume(ws);
|
|
1822
|
+
});
|
|
1823
|
+
ws.addEventListener("message", (ev) => {
|
|
1824
|
+
this.#onMessage(JSON.parse(ev.data));
|
|
1825
|
+
});
|
|
1826
|
+
ws.addEventListener("close", () => {
|
|
1827
|
+
this.#connecting = false;
|
|
1828
|
+
if (this.#ws === ws) this.#ws = void 0;
|
|
1829
|
+
this.#poke = null;
|
|
1830
|
+
this.#scheduleReconnect();
|
|
1831
|
+
});
|
|
1832
|
+
ws.addEventListener("error", () => ws.close());
|
|
1833
|
+
}
|
|
1834
|
+
/** Resubscribe active queries and resend unconfirmed mutations. */
|
|
1835
|
+
#resume(ws) {
|
|
1836
|
+
const queries = this.#queries.active();
|
|
1837
|
+
if (queries.length) {
|
|
1838
|
+
ws.send(JSON.stringify(["changeDesiredQueries", { desiredQueriesPatch: queries }]));
|
|
1839
|
+
}
|
|
1840
|
+
for (const msg of this.#unconfirmedPushes.values()) ws.send(JSON.stringify(msg));
|
|
1841
|
+
}
|
|
1842
|
+
#scheduleReconnect() {
|
|
1843
|
+
if (this.#closed || this.#maxReconnectMs === 0) return;
|
|
1844
|
+
const delay = Math.min(this.#reconnectMs, this.#maxReconnectMs);
|
|
1845
|
+
this.#reconnectMs = Math.min(this.#reconnectMs * 2, this.#maxReconnectMs);
|
|
1846
|
+
setTimeout(() => void this.#connect(), delay);
|
|
1847
|
+
}
|
|
1848
|
+
#send(msg) {
|
|
1849
|
+
if (this.#ws && this.#ws.readyState === WebSocket.OPEN) {
|
|
1850
|
+
this.#ws.send(JSON.stringify(msg));
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
#onMessage(msg) {
|
|
1854
|
+
const [tag, body] = msg;
|
|
1855
|
+
switch (tag) {
|
|
1856
|
+
case "pokeStart":
|
|
1857
|
+
this.#poke = { rows: [], lmids: {} };
|
|
1858
|
+
return;
|
|
1859
|
+
case "pokePart": {
|
|
1860
|
+
const poke = this.#poke ??= { rows: [], lmids: {} };
|
|
1861
|
+
if (body.rowsPatch) poke.rows.push(...body.rowsPatch);
|
|
1862
|
+
if (body.lastMutationIDChanges) Object.assign(poke.lmids, body.lastMutationIDChanges);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
case "pokeEnd": {
|
|
1866
|
+
const poke = this.#poke;
|
|
1867
|
+
this.#poke = null;
|
|
1868
|
+
if (!poke || body.cancel) return;
|
|
1869
|
+
if (poke.rows.length) this.#store.applyAll(poke.rows);
|
|
1870
|
+
const confirmed = poke.lmids[this.clientID];
|
|
1871
|
+
if (confirmed != null) {
|
|
1872
|
+
for (const id of this.#unconfirmedPushes.keys()) {
|
|
1873
|
+
if (id <= confirmed) this.#unconfirmedPushes.delete(id);
|
|
1874
|
+
}
|
|
1875
|
+
this.#store.confirmThrough(confirmed);
|
|
1876
|
+
}
|
|
1877
|
+
this.#cookie = body.cookie;
|
|
1878
|
+
void this.#kv?.set("cookie", body.cookie);
|
|
1879
|
+
this.#reconnectMs = 500;
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
case "error":
|
|
1883
|
+
if (this.#onError) this.#onError(body);
|
|
1884
|
+
else console.error(`orbit: server error: ${body.kind}: ${body.message}`);
|
|
1885
|
+
return;
|
|
1886
|
+
default:
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
// client/src/persist.ts
|
|
1893
|
+
var MemoryKV = class {
|
|
1894
|
+
#m = /* @__PURE__ */ new Map();
|
|
1895
|
+
async get(key) {
|
|
1896
|
+
return this.#m.get(key);
|
|
1897
|
+
}
|
|
1898
|
+
async set(key, value) {
|
|
1899
|
+
this.#m.set(key, value);
|
|
1900
|
+
}
|
|
1901
|
+
async del(key) {
|
|
1902
|
+
this.#m.delete(key);
|
|
1903
|
+
}
|
|
1904
|
+
async entries(prefix) {
|
|
1905
|
+
return [...this.#m].filter(([k]) => k.startsWith(prefix));
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
function wrap(req) {
|
|
1909
|
+
return new Promise((resolve2, reject) => {
|
|
1910
|
+
req.onsuccess = () => resolve2(req.result);
|
|
1911
|
+
req.onerror = () => reject(req.error);
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
var IDBKV = class {
|
|
1915
|
+
#dbp;
|
|
1916
|
+
constructor(name = "orbit") {
|
|
1917
|
+
this.#dbp = new Promise((resolve2, reject) => {
|
|
1918
|
+
const r = indexedDB.open(name, 1);
|
|
1919
|
+
r.onupgradeneeded = () => r.result.createObjectStore("kv");
|
|
1920
|
+
r.onsuccess = () => resolve2(r.result);
|
|
1921
|
+
r.onerror = () => reject(r.error);
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
async #store(mode) {
|
|
1925
|
+
const db = await this.#dbp;
|
|
1926
|
+
return db.transaction("kv", mode).objectStore("kv");
|
|
1927
|
+
}
|
|
1928
|
+
async get(key) {
|
|
1929
|
+
return wrap((await this.#store("readonly")).get(key));
|
|
1930
|
+
}
|
|
1931
|
+
async set(key, value) {
|
|
1932
|
+
await wrap((await this.#store("readwrite")).put(value, key));
|
|
1933
|
+
}
|
|
1934
|
+
async del(key) {
|
|
1935
|
+
await wrap((await this.#store("readwrite")).delete(key));
|
|
1936
|
+
}
|
|
1937
|
+
async entries(prefix) {
|
|
1938
|
+
const s = await this.#store("readonly");
|
|
1939
|
+
const [keys, vals] = await Promise.all([wrap(s.getAllKeys()), wrap(s.getAll())]);
|
|
1940
|
+
const out = [];
|
|
1941
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1942
|
+
const k = String(keys[i]);
|
|
1943
|
+
if (k.startsWith(prefix)) out.push([k, vals[i]]);
|
|
1944
|
+
}
|
|
1945
|
+
return out;
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1949
|
+
export { IDBKV, MaterializedView, MemoryKV, MemorySource, MemorySourceProvider, Orbit, PROTOCOL_VERSION, Query, QueryManager, SchemaQuery, SourceConnection, Store, StoreProvider, TypedQuery, View, boolean, buildPipeline, buildSchemaQueries, collectOps, compareValues, createBuilder, createSchema, defineMutator, defineQuery, evaluate, hashAST, hashString, json, nodeToRow, number, optional, parseTTL, relationships, string, table, tablesOf, unwrapSingular, valuesEqual };
|
|
1950
|
+
//# sourceMappingURL=chunk-N2NAKHMU.js.map
|
|
1951
|
+
//# sourceMappingURL=chunk-N2NAKHMU.js.map
|