@ultimat3/query 2.0.0 → 3.0.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/CLAUDE.md +11 -0
- package/package.json +6 -6
- package/src/matcher.ts +21 -5
package/CLAUDE.md
CHANGED
|
@@ -229,6 +229,17 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
|
|
|
229
229
|
`"null"`. Refused: a structural member (`object`, `record`, `money`, or an array/union of one),
|
|
230
230
|
a REQUIRED nullable member, and a top-level input that is not an object. A schema
|
|
231
231
|
`tryIntrospect` cannot read is left alone, or `configureSchemaProvider` would be unusable.
|
|
232
|
+
- **A refill is owed by a FULL window and by nothing else** (`matcher.ts`, `As of 2026-08`).
|
|
233
|
+
`removeAt` pushed one whenever `shape.limit !== null`, with no reference to how many rows the
|
|
234
|
+
window holds: three rows under `limit: 50`, delete one, and the patch list was
|
|
235
|
+
`[{remove, position:1}, {refill, from:49}]` — a position no two-row result set has. It is not a
|
|
236
|
+
harmless extra: `@ultimat3/realtime`'s `matcher-bridge` folds any refill into
|
|
237
|
+
`BridgeResult.refill`, and `live-fanout` then sends **no patch frame at all** that round, marking
|
|
238
|
+
every subscriber desynced instead — so on a quiet feed the deleted row stays rendered until some
|
|
239
|
+
other change to the same query id arrives, and on a busy one it is a full DB re-read plus one
|
|
240
|
+
snapshot per subscriber per delete. A window under `limit` has no unknown tail: the source served
|
|
241
|
+
fewer rows than it was allowed to, so what the client holds IS the result set. `held >=
|
|
242
|
+
shape.limit` is the gate, and it is `wasFull` one branch away, already written.
|
|
232
243
|
- **A move OUT of a full window is a `refill`, never an `add`** (`matcher.ts`). `insert()` places a
|
|
233
244
|
moved row among the `limit - 1` rows the client still holds, so its position can never reach
|
|
234
245
|
`shape.limit` and the `position >= shape.limit` bail is unreachable on that path — the row was
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/query",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/cache": "
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/http": "
|
|
37
|
-
"@ultimat3/policy": "
|
|
38
|
-
"@ultimat3/schema": "
|
|
34
|
+
"@ultimat3/cache": "3.0.0",
|
|
35
|
+
"@ultimat3/core": "3.0.0",
|
|
36
|
+
"@ultimat3/http": "3.0.0",
|
|
37
|
+
"@ultimat3/policy": "3.0.0",
|
|
38
|
+
"@ultimat3/schema": "3.0.0"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/matcher.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function match<TRow extends object>(
|
|
|
61
61
|
const belongs = event.op !== 'delete' && matchesFilters(event.row, shape.filters);
|
|
62
62
|
|
|
63
63
|
if (event.op === 'delete' || (inSet && !belongs)) {
|
|
64
|
-
return inSet ? removeAt(shape, index, id, true) : [];
|
|
64
|
+
return inSet ? removeAt(shape, index, id, true, rows.length) : [];
|
|
65
65
|
}
|
|
66
66
|
if (!belongs) return [];
|
|
67
67
|
if (!inSet) return insert(shape, rows, event.row);
|
|
@@ -84,9 +84,12 @@ export function match<TRow extends object>(
|
|
|
84
84
|
// too, so the refill covers both.
|
|
85
85
|
const wasFull = shape.limit !== null && rows.length >= shape.limit;
|
|
86
86
|
if (wasFull && positionFor(shape, without, event.row) >= without.length) {
|
|
87
|
-
return removeAt<TRow>(shape, index, id, true);
|
|
87
|
+
return removeAt<TRow>(shape, index, id, true, rows.length);
|
|
88
88
|
}
|
|
89
|
-
return [
|
|
89
|
+
return [
|
|
90
|
+
...removeAt<TRow>(shape, index, id, false, rows.length),
|
|
91
|
+
...insert(shape, without, event.row),
|
|
92
|
+
];
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
function insert<TRow extends object>(
|
|
@@ -107,15 +110,28 @@ function insert<TRow extends object>(
|
|
|
107
110
|
return patches;
|
|
108
111
|
}
|
|
109
112
|
|
|
113
|
+
/**
|
|
114
|
+
* `held` is how many rows the window actually holds, and it is the whole condition on the refill.
|
|
115
|
+
*
|
|
116
|
+
* A refill says the tail is unknown to the client, and it is answered by a full re-read: the bridge
|
|
117
|
+
* folds it into `BridgeResult.refill`, and the fanout then sends NO patch frame that round —
|
|
118
|
+
* suppressing the `remove` beside it and leaving a deleted row on screen until the next change to
|
|
119
|
+
* the same query. A window under `limit` has no unknown tail: the source served fewer rows than it
|
|
120
|
+
* was allowed to, so what the client holds IS the result set. Unconditional, this also named a
|
|
121
|
+
* position no result set has — `limit: 50` over three rows emitted `{ refill, from: 49 }`.
|
|
122
|
+
*/
|
|
110
123
|
function removeAt<TRow extends object>(
|
|
111
124
|
shape: QueryShape,
|
|
112
125
|
index: number,
|
|
113
126
|
id: string,
|
|
114
127
|
refill: boolean,
|
|
128
|
+
held: number,
|
|
115
129
|
): readonly Patch<TRow>[] {
|
|
116
130
|
const patches: Patch<TRow>[] = [{ kind: 'remove', position: index, id }];
|
|
117
|
-
// A
|
|
118
|
-
if (refill && shape.limit !== null
|
|
131
|
+
// A window that WAS full is now one row short, and that row lives on the server.
|
|
132
|
+
if (refill && shape.limit !== null && held >= shape.limit) {
|
|
133
|
+
patches.push({ kind: 'refill', from: shape.limit - 1 });
|
|
134
|
+
}
|
|
119
135
|
return patches;
|
|
120
136
|
}
|
|
121
137
|
|