foldkit-remote 0.20.0 → 0.22.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/README.md +987 -198
- package/dist/answerGaps.d.ts +16 -0
- package/dist/answerGaps.d.ts.map +1 -0
- package/dist/answerGaps.js +60 -0
- package/dist/answerGaps.js.map +1 -0
- package/dist/client-B055kQuP.d.ts +110 -0
- package/dist/client-B055kQuP.d.ts.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +142 -0
- package/dist/client.js.map +1 -0
- package/dist/coalesce-CLAr92Nk.d.ts +34 -0
- package/dist/coalesce-CLAr92Nk.d.ts.map +1 -0
- package/dist/coalesce.d.ts +2 -0
- package/dist/coalesce.js +131 -0
- package/dist/coalesce.js.map +1 -0
- package/dist/connection-BRyaJy89.d.ts +45 -0
- package/dist/connection-BRyaJy89.d.ts.map +1 -0
- package/dist/connection.d.ts +2 -0
- package/dist/connection.js +170 -0
- package/dist/connection.js.map +1 -0
- package/dist/data.d.ts +7 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +21 -0
- package/dist/data.js.map +1 -0
- package/dist/diagnostics-DHhVYflV.d.ts +125 -0
- package/dist/diagnostics-DHhVYflV.d.ts.map +1 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +60 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/entity-CgsG5Uel.d.ts +196 -0
- package/dist/entity-CgsG5Uel.d.ts.map +1 -0
- package/dist/entity-DuDxaa3n.js +279 -0
- package/dist/entity-DuDxaa3n.js.map +1 -0
- package/dist/entity.d.ts +2 -0
- package/dist/entity.js +1 -0
- package/dist/held.d.ts +19 -0
- package/dist/held.d.ts.map +1 -0
- package/dist/held.js +70 -0
- package/dist/held.js.map +1 -0
- package/dist/impact.d.ts +79 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +166 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +1542 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2791 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect-DtRQGCRP.d.ts +134 -0
- package/dist/inspect-DtRQGCRP.d.ts.map +1 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +33 -0
- package/dist/inspect.js.map +1 -0
- package/dist/json-DowLl1eB.d.ts +52 -0
- package/dist/json-DowLl1eB.d.ts.map +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +123 -0
- package/dist/json.js.map +1 -0
- package/dist/law.d.ts +56 -0
- package/dist/law.d.ts.map +1 -0
- package/dist/law.js +201 -0
- package/dist/law.js.map +1 -0
- package/dist/live-BpTzlCnY.d.ts +79 -0
- package/dist/live-BpTzlCnY.d.ts.map +1 -0
- package/dist/live.d.ts +2 -0
- package/dist/live.js +126 -0
- package/dist/live.js.map +1 -0
- package/dist/matching-DF5F9Us8.d.ts +100 -0
- package/dist/matching-DF5F9Us8.d.ts.map +1 -0
- package/dist/matching.d.ts +2 -0
- package/dist/matching.js +172 -0
- package/dist/matching.js.map +1 -0
- package/dist/model-B6zk1NMB.d.ts +710 -0
- package/dist/model-B6zk1NMB.d.ts.map +1 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +1308 -0
- package/dist/model.js.map +1 -0
- package/dist/mutation.d.ts +2 -0
- package/dist/mutation.js +612 -0
- package/dist/mutation.js.map +1 -0
- package/dist/optimistic-CECsfNGY.d.ts +523 -0
- package/dist/optimistic-CECsfNGY.d.ts.map +1 -0
- package/dist/optimistic.d.ts +2 -0
- package/dist/optimistic.js +336 -0
- package/dist/optimistic.js.map +1 -0
- package/dist/outbox-DNuc4kZp.d.ts +259 -0
- package/dist/outbox-DNuc4kZp.d.ts.map +1 -0
- package/dist/outbox.d.ts +2 -0
- package/dist/outbox.js +326 -0
- package/dist/outbox.js.map +1 -0
- package/dist/patchValues.d.ts +10 -0
- package/dist/patchValues.d.ts.map +1 -0
- package/dist/patchValues.js +57 -0
- package/dist/patchValues.js.map +1 -0
- package/dist/persistence-CCMnvg7-.d.ts +114 -0
- package/dist/persistence-CCMnvg7-.d.ts.map +1 -0
- package/dist/persistence.d.ts +2 -0
- package/dist/persistence.js +205 -0
- package/dist/persistence.js.map +1 -0
- package/dist/placement-jZdsIpmj.d.ts +50 -0
- package/dist/placement-jZdsIpmj.d.ts.map +1 -0
- package/dist/placement.d.ts +2 -0
- package/dist/placement.js +116 -0
- package/dist/placement.js.map +1 -0
- package/dist/plan-D9cftxC_.d.ts +35 -0
- package/dist/plan-D9cftxC_.d.ts.map +1 -0
- package/dist/plan.d.ts +2 -0
- package/dist/plan.js +100 -0
- package/dist/plan.js.map +1 -0
- package/dist/policy-DsWHmIAq.d.ts +42 -0
- package/dist/policy-DsWHmIAq.d.ts.map +1 -0
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js +32 -0
- package/dist/policy.js.map +1 -0
- package/dist/{port.d.mts → port.d.ts} +3 -2
- package/dist/port.d.ts.map +1 -0
- package/dist/{port.mjs → port.js} +3 -2
- package/dist/port.js.map +1 -0
- package/dist/query-BuIU93xA.d.ts +141 -0
- package/dist/query-BuIU93xA.d.ts.map +1 -0
- package/dist/query.d.ts +2 -0
- package/dist/query.js +0 -0
- package/dist/query.js.map +1 -0
- package/dist/record.d.ts +11 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +21 -0
- package/dist/record.js.map +1 -0
- package/dist/relation-BLBGOZom.d.ts +59 -0
- package/dist/relation-BLBGOZom.d.ts.map +1 -0
- package/dist/relation.d.ts +2 -0
- package/dist/relation.js +93 -0
- package/dist/relation.js.map +1 -0
- package/dist/remoteData-mK1suYi1.d.ts +148 -0
- package/dist/remoteData-mK1suYi1.d.ts.map +1 -0
- package/dist/remoteData.d.ts +2 -0
- package/dist/remoteData.js +179 -0
- package/dist/remoteData.js.map +1 -0
- package/dist/reporting.d.ts +12 -0
- package/dist/reporting.d.ts.map +1 -0
- package/dist/reporting.js +45 -0
- package/dist/reporting.js.map +1 -0
- package/dist/requirement-D57S2LpH.d.ts +64 -0
- package/dist/requirement-D57S2LpH.d.ts.map +1 -0
- package/dist/requirement.d.ts +2 -0
- package/dist/requirement.js +114 -0
- package/dist/requirement.js.map +1 -0
- package/dist/resume-Ct5R6yli.d.ts +92 -0
- package/dist/resume-Ct5R6yli.d.ts.map +1 -0
- package/dist/resume.d.ts +2 -0
- package/dist/resume.js +222 -0
- package/dist/resume.js.map +1 -0
- package/dist/retain-BfNTvBRb.d.ts +48 -0
- package/dist/retain-BfNTvBRb.d.ts.map +1 -0
- package/dist/retain.d.ts +2 -0
- package/dist/retain.js +107 -0
- package/dist/retain.js.map +1 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +1 -0
- package/dist/shown.d.ts +20 -0
- package/dist/shown.d.ts.map +1 -0
- package/dist/shown.js +40 -0
- package/dist/shown.js.map +1 -0
- package/dist/sse-C7LhdbiQ.d.ts +17 -0
- package/dist/sse-C7LhdbiQ.d.ts.map +1 -0
- package/dist/sse.d.ts +2 -0
- package/dist/sse.js +127 -0
- package/dist/sse.js.map +1 -0
- package/dist/store-ah2qjcJW.d.ts +100 -0
- package/dist/store-ah2qjcJW.d.ts.map +1 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +218 -0
- package/dist/store.js.map +1 -0
- package/dist/testing.d.ts +133 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +0 -0
- package/dist/testing.js.map +1 -0
- package/dist/why-kgp_Wr3z.d.ts +483 -0
- package/dist/why-kgp_Wr3z.d.ts.map +1 -0
- package/dist/why.d.ts +2 -0
- package/dist/why.js +293 -0
- package/dist/why.js.map +1 -0
- package/dist/whyReasons.d.ts +42 -0
- package/dist/whyReasons.d.ts.map +1 -0
- package/dist/whyReasons.js +211 -0
- package/dist/whyReasons.js.map +1 -0
- package/dist/wire-DthPcTM6.d.ts +787 -0
- package/dist/wire-DthPcTM6.d.ts.map +1 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.js +329 -0
- package/dist/wire.js.map +1 -0
- package/package.json +19 -14
- package/dist/index-BEvVAL76.d.mts +0 -4097
- package/dist/index-BEvVAL76.d.mts.map +0 -1
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/port.d.mts.map +0 -1
- package/dist/port.mjs.map +0 -1
- package/dist/src-BDqn7Ocr.mjs +0 -6344
- package/dist/src-BDqn7Ocr.mjs.map +0 -1
package/dist/matching.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { holderOf } from "./held.js";
|
|
2
|
+
import { Option, Schema } from "effect";
|
|
3
|
+
import { Held, Judge, Query, QueryEvaluateError, evaluateEach } from "foldkit-entity";
|
|
4
|
+
// src/matching.ts
|
|
5
|
+
/**
|
|
6
|
+
* Running a query body against the rows the client already holds.
|
|
7
|
+
*
|
|
8
|
+
* local-execution-DESIGN phase 3. A connection's rows are edges the server
|
|
9
|
+
* delivered, and until now nothing on the client ever evaluated a body — which
|
|
10
|
+
* is why an optimistic insert has to guess between `prepend` and `append`, and
|
|
11
|
+
* why `LivePolicy` exists to be told in advance what to do with a row nobody
|
|
12
|
+
* could judge.
|
|
13
|
+
*
|
|
14
|
+
* This judges them. It is pure, it performs no I/O, and it reuses the reference
|
|
15
|
+
* interpreter in `foldkit-entity` rather than reimplementing the semantics —
|
|
16
|
+
* so a body means here exactly what it means on the server, checked by the same
|
|
17
|
+
* conformance suite.
|
|
18
|
+
*
|
|
19
|
+
* **It answers "which of the rows I hold match", never "which rows match".**
|
|
20
|
+
* Those are different questions and the difference is not recoverable from the
|
|
21
|
+
* result, so it is in the shape: `matched` is what satisfied the body, and
|
|
22
|
+
* `skipped` is what was held but could not be judged. A caller that knows it
|
|
23
|
+
* holds the whole population — a connection terminal at both ends — is the one
|
|
24
|
+
* that can turn this into a complete answer. Nothing here can.
|
|
25
|
+
*
|
|
26
|
+
* **It is not authorization.** On the server a compiled `where` is conjoined
|
|
27
|
+
* with the binding's `visible` rule. There is no `visible` here, and this is
|
|
28
|
+
* safe only because the client holds only rows the server already released to
|
|
29
|
+
* it. A local filter is not an access decision.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The fields of the body's own Entity that a row must have for the body to be
|
|
33
|
+
* decidable about it — its predicates and its ordering terms.
|
|
34
|
+
*
|
|
35
|
+
* `id` is never among them: it is the key, so it is known for every row in the
|
|
36
|
+
* store whether or not anything fetched it as a field.
|
|
37
|
+
*/
|
|
38
|
+
const decidableOn = (body) => Query.dependencies(body).fields.filter((field) => field.entity === body.entity.name && field.key !== "id").map((field) => field.key);
|
|
39
|
+
/**
|
|
40
|
+
* The rows of `descriptor`'s Entity that the store holds and the body matches.
|
|
41
|
+
*
|
|
42
|
+
* The input is given **decoded**, as an application writes it, and encoded here
|
|
43
|
+
* through the descriptor's own `Input` codec. That is deliberate and is the
|
|
44
|
+
* whole reason this takes a descriptor rather than a bare body: a store holds
|
|
45
|
+
* wire values, so an input has to be in that space to be compared against them,
|
|
46
|
+
* and handing an interpreter a decoded value is a mistake **no runtime error
|
|
47
|
+
* catches** — the reference interpreter answers "no rows", which reads exactly
|
|
48
|
+
* like a correct answer. Doing it here means a caller cannot get it wrong.
|
|
49
|
+
*
|
|
50
|
+
* Literals in the body are deliberately *not* encoded. A literal written in
|
|
51
|
+
* domain terms is already broken against the server, which binds it to a column
|
|
52
|
+
* the same way; accepting it here would make the client quietly more permissive
|
|
53
|
+
* than the thing it has to agree with.
|
|
54
|
+
*/
|
|
55
|
+
const matching = (store, descriptor, input, options = {}) => matchingEncoded(store, descriptor, Schema.encodeSync(descriptor.Input)(input), options);
|
|
56
|
+
/**
|
|
57
|
+
* As `matching`, for the callers in this module that already hold the input
|
|
58
|
+
* **encoded**. Not exported: `matching` is the way in, and it cannot be given
|
|
59
|
+
* the wrong space.
|
|
60
|
+
*/
|
|
61
|
+
const matchingEncoded = (store, descriptor, encoded, options = {}) => {
|
|
62
|
+
const body = descriptor.body;
|
|
63
|
+
if (body === void 0) throw new Error(`[foldkit-remote] Remote.matching: query "${descriptor.name}" carries no body, so there is nothing to judge rows against. Declare it with Query.define.`);
|
|
64
|
+
const prefix = `${body.entity.name}:`;
|
|
65
|
+
const needed = decidableOn(body);
|
|
66
|
+
const candidates = options.among ?? Object.keys(store).filter((key) => key.startsWith(prefix));
|
|
67
|
+
const rows = [];
|
|
68
|
+
const skipped = [];
|
|
69
|
+
for (const key of candidates) {
|
|
70
|
+
if (!key.startsWith(prefix)) continue;
|
|
71
|
+
const entry = store[key];
|
|
72
|
+
if (entry === void 0) {
|
|
73
|
+
skipped.push(key);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (entry.tombstone) continue;
|
|
77
|
+
if (needed.some((field) => !entry.present.has(field))) {
|
|
78
|
+
skipped.push(key);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const values = Held.fromRefs(body, entry.values);
|
|
82
|
+
rows.push("id" in values ? {
|
|
83
|
+
...values,
|
|
84
|
+
__key: key
|
|
85
|
+
} : {
|
|
86
|
+
...values,
|
|
87
|
+
id: key.slice(prefix.length),
|
|
88
|
+
__key: key
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const reaching = reachesOtherRows(body);
|
|
92
|
+
const holder = reaching ? holderOf(store) : void 0;
|
|
93
|
+
const admitted = holder === void 0 ? rows : rows.filter((row) => Option.match(judgedOver(holder, body, encoded, row.__key, prefix, options), {
|
|
94
|
+
onSome: (matched) => matched,
|
|
95
|
+
onNone: () => {
|
|
96
|
+
skipped.push(row.__key);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
const judged = evaluateEach(reaching ? orderOnly(body) : body, encoded, admitted, {
|
|
101
|
+
operators: options.operators,
|
|
102
|
+
context: options.context
|
|
103
|
+
});
|
|
104
|
+
const keyOf = (row) => row.__key;
|
|
105
|
+
return {
|
|
106
|
+
matched: judged.matched.map(keyOf),
|
|
107
|
+
skipped: [...skipped, ...judged.refused.map(keyOf)]
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const reachingBodies = /* @__PURE__ */ new WeakMap();
|
|
111
|
+
/** Whether a body's predicates read other rows: a path, an exists, a path from the viewer. */
|
|
112
|
+
const reachesOtherRows = (body) => {
|
|
113
|
+
const known = reachingBodies.get(body);
|
|
114
|
+
if (known !== void 0) return known;
|
|
115
|
+
const reaching = Query.dependencies(body).reaches.length > 0;
|
|
116
|
+
reachingBodies.set(body, reaching);
|
|
117
|
+
return reaching;
|
|
118
|
+
};
|
|
119
|
+
const ordersOf = /* @__PURE__ */ new WeakMap();
|
|
120
|
+
/** `body`'s order without its predicates, to put rows it admitted in order. */
|
|
121
|
+
const orderOnly = (body) => {
|
|
122
|
+
const known = ordersOf.get(body);
|
|
123
|
+
if (known !== void 0) return known;
|
|
124
|
+
const ordered = Query.from(body.entity).pipe(Query.orderBy(...body.orderBy));
|
|
125
|
+
ordersOf.set(body, ordered);
|
|
126
|
+
return ordered;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Whether the held row under `key` is in `body`'s result, judged over the
|
|
130
|
+
* whole store: none where the answer rests on what the store lacks.
|
|
131
|
+
*/
|
|
132
|
+
const judgedOver = (holder, body, encoded, key, prefix, options) => Option.flatMap(holder.rows.row(body.entity, key.slice(prefix.length)), (row) => Judge.match(Judge.query(body, encoded, row, {
|
|
133
|
+
operators: options.operators,
|
|
134
|
+
context: options.context,
|
|
135
|
+
rows: holder.rows
|
|
136
|
+
}), {
|
|
137
|
+
onKnown: Option.some,
|
|
138
|
+
onUnknown: () => Option.none()
|
|
139
|
+
}));
|
|
140
|
+
/**
|
|
141
|
+
* Takes the input **encoded**, because its one caller holds it that way: a
|
|
142
|
+
* connection identity carries the canonical encoded input, and making it decode
|
|
143
|
+
* only for this to encode again would be a round trip that can fail.
|
|
144
|
+
*
|
|
145
|
+
* A decoded-input sibling was written first and deleted: nothing called it. The
|
|
146
|
+
* asymmetry with `matching`, which takes a decoded input, is the shape of the
|
|
147
|
+
* two real callers rather than an oversight.
|
|
148
|
+
*/
|
|
149
|
+
const belongsEncoded = (store, descriptor, encoded, key, operators, context) => {
|
|
150
|
+
const entry = store[key];
|
|
151
|
+
if (entry === void 0) return "unknown";
|
|
152
|
+
if (entry.tombstone) return "no";
|
|
153
|
+
const body = descriptor.body;
|
|
154
|
+
if (body !== void 0 && decidableOn(body).some((field) => entry.stale.has(field))) return "unknown";
|
|
155
|
+
let judged;
|
|
156
|
+
try {
|
|
157
|
+
judged = matchingEncoded(store, descriptor, encoded, {
|
|
158
|
+
among: [key],
|
|
159
|
+
operators,
|
|
160
|
+
context
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error instanceof QueryEvaluateError) return "unknown";
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
if (judged.matched.includes(key)) return "yes";
|
|
167
|
+
if (judged.skipped.includes(key)) return "unknown";
|
|
168
|
+
return "no";
|
|
169
|
+
};
|
|
170
|
+
export { belongsEncoded, matching };
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=matching.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matching.js","names":["Relational"],"sources":["../src/matching.ts"],"sourcesContent":["/**\n * Running a query body against the rows the client already holds.\n *\n * local-execution-DESIGN phase 3. A connection's rows are edges the server\n * delivered, and until now nothing on the client ever evaluated a body — which\n * is why an optimistic insert has to guess between `prepend` and `append`, and\n * why `LivePolicy` exists to be told in advance what to do with a row nobody\n * could judge.\n *\n * This judges them. It is pure, it performs no I/O, and it reuses the reference\n * interpreter in `foldkit-entity` rather than reimplementing the semantics —\n * so a body means here exactly what it means on the server, checked by the same\n * conformance suite.\n *\n * **It answers \"which of the rows I hold match\", never \"which rows match\".**\n * Those are different questions and the difference is not recoverable from the\n * result, so it is in the shape: `matched` is what satisfied the body, and\n * `skipped` is what was held but could not be judged. A caller that knows it\n * holds the whole population — a connection terminal at both ends — is the one\n * that can turn this into a complete answer. Nothing here can.\n *\n * **It is not authorization.** On the server a compiled `where` is conjoined\n * with the binding's `visible` rule. There is no `visible` here, and this is\n * safe only because the client holds only rows the server already released to\n * it. A local filter is not an access decision.\n */\nimport { Option, Schema } from 'effect'\nimport { Query as Relational } from 'foldkit-entity'\nimport {\n Held,\n Judge,\n QueryEvaluateError,\n evaluateEach,\n type AnyQuery,\n type ExprContext,\n type MemoryOperators,\n type Row,\n} from 'foldkit-entity'\nimport { holderOf, type StoreHolder } from './held.js'\nimport type { QueryDescriptor } from './query.js'\nimport type { EntityKey, EntityStore } from './store.js'\n\n/**\n * An answer over the rows the client holds — never over the rows that exist.\n *\n * `complete` is the whole reason this is its own type rather than a `Page`. A\n * `Page` carries `hasNext`/`hasPrevious`, which are facts the *server* stated\n * about rows beyond the ones it delivered; a local answer has no such facts and\n * cannot invent them. What it can say is whether the population it judged was\n * all of it, which is decidable: a connection terminal at both ends is wholly\n * held, and nothing was skipped for a missing field.\n *\n * An incomplete answer is not wrong — it is an answer about less than the\n * caller may have meant, and the flag is what lets a view say \"3 so far\" rather\n * than \"3\", or a caller decide to ask the server.\n */\nexport interface Matched<A> {\n readonly items: ReadonlyArray<A>\n /** Whether every row the answer is about was judged. */\n readonly complete: boolean\n}\n\n/**\n * What the rows the client holds had to say about a body.\n *\n * `skipped` is the honest half. A row missing a field the body reads cannot be\n * judged — it is neither a match nor a non-match — and dropping it silently\n * would turn \"I could not tell\" into \"no\". Naming it lets a caller decide:\n * fetch the field, ask the server, or say the answer is partial.\n */\nexport interface Judged {\n /** Keys of the rows that satisfy the body, in the order the body asks for. */\n readonly matched: ReadonlyArray<EntityKey>\n /**\n * Keys of rows held but not judged either way: missing a field the body\n * reads, or holding a value the interpreter refuses (text with a NUL).\n */\n readonly skipped: ReadonlyArray<EntityKey>\n}\n\n/**\n * The fields of the body's own Entity that a row must have for the body to be\n * decidable about it — its predicates and its ordering terms.\n *\n * `id` is never among them: it is the key, so it is known for every row in the\n * store whether or not anything fetched it as a field.\n */\nconst decidableOn = (body: Parameters<typeof Relational.dependencies>[0]): ReadonlyArray<string> =>\n Relational.dependencies(body)\n .fields.filter(field => field.entity === body.entity.name && field.key !== 'id')\n .map(field => field.key)\n\nexport interface MatchingOptions {\n /**\n * Judge only these keys. Without it every row of the body's Entity in the\n * store is a candidate, which is what a filter over a loaded list wants; with\n * it, a caller narrows to a connection's own edges.\n */\n readonly among?: ReadonlyArray<EntityKey> | undefined\n /** How the body's primitive operators are run: `Remote.make`'s `operators`. */\n readonly operators?: MemoryOperators | undefined\n /**\n * What the body's `Expr.viewer` and `Expr.now` read: `Remote.make`'s\n * `viewer`. Without it, or without a reading of the time, a body reading\n * one cannot be judged here.\n */\n readonly context?: ExprContext | undefined\n}\n\n/**\n * The rows of `descriptor`'s Entity that the store holds and the body matches.\n *\n * The input is given **decoded**, as an application writes it, and encoded here\n * through the descriptor's own `Input` codec. That is deliberate and is the\n * whole reason this takes a descriptor rather than a bare body: a store holds\n * wire values, so an input has to be in that space to be compared against them,\n * and handing an interpreter a decoded value is a mistake **no runtime error\n * catches** — the reference interpreter answers \"no rows\", which reads exactly\n * like a correct answer. Doing it here means a caller cannot get it wrong.\n *\n * Literals in the body are deliberately *not* encoded. A literal written in\n * domain terms is already broken against the server, which binds it to a column\n * the same way; accepting it here would make the client quietly more permissive\n * than the thing it has to agree with.\n */\nexport const matching = <Name extends string, Input>(\n store: EntityStore,\n descriptor: QueryDescriptor<Name, Input, unknown>,\n input: Input,\n options: MatchingOptions = {},\n): Judged =>\n matchingEncoded(\n store,\n descriptor,\n Schema.encodeSync(descriptor.Input)(input) as Readonly<Record<string, unknown>>,\n options,\n )\n\n/**\n * As `matching`, for the callers in this module that already hold the input\n * **encoded**. Not exported: `matching` is the way in, and it cannot be given\n * the wrong space.\n */\nconst matchingEncoded = <Name extends string, Input>(\n store: EntityStore,\n descriptor: QueryDescriptor<Name, Input, unknown>,\n encoded: Readonly<Record<string, unknown>>,\n options: MatchingOptions = {},\n): Judged => {\n const body = descriptor.body\n if (body === undefined) {\n throw new Error(\n `[foldkit-remote] Remote.matching: query \"${descriptor.name}\" carries no body, so there is nothing to judge rows against. Declare it with Query.define.`,\n )\n }\n\n const entity = body.entity.name\n const prefix = `${entity}:`\n const needed = decidableOn(body)\n\n const candidates = options.among ?? Object.keys(store).filter(key => key.startsWith(prefix))\n\n const rows: Array<Row & { readonly __key: EntityKey }> = []\n const skipped: EntityKey[] = []\n\n for (const key of candidates) {\n if (!key.startsWith(prefix)) continue\n const entry = store[key]\n // Not held at all: nothing to judge. Reachable only through `among`, where\n // a caller named a key the store does not have — and a key nobody could\n // judge belongs in `skipped`, not in silence, or an answer built from it\n // would claim to have considered it.\n if (entry === undefined) {\n skipped.push(key)\n continue\n }\n // A tombstone is not a row. Known absent is an answer, not an absence of\n // one, so it is neither matched nor skipped.\n if (entry.tombstone) continue\n if (needed.some(field => !entry.present.has(field))) {\n skipped.push(key)\n continue\n }\n // The key's id is a **fallback**, not the truth: `Entity.ref` stringifies\n // ids, so a numeric id is `7` in the store and `\"7\"` in the key. Overwriting\n // the encoded value with the key's makes `eq(id, 7)` false and sorts ids\n // lexicographically — both silently.\n // The store keeps a link as its ref key; the body reads the id it points at.\n const values = Held.fromRefs(body, entry.values)\n rows.push(\n 'id' in values\n ? { ...values, __key: key }\n : { ...values, id: key.slice(prefix.length), __key: key },\n )\n }\n\n // A body reading other rows is judged over the store as a holder, three\n // valued: a row whose answer rests on what the store lacks is skipped.\n // What it admits is then put in order by its terms alone.\n const reaching = reachesOtherRows(body)\n const holder = reaching ? holderOf(store) : undefined\n const admitted =\n holder === undefined\n ? rows\n : rows.filter(row =>\n Option.match(judgedOver(holder, body, encoded, row.__key, prefix, options), {\n onSome: matched => matched,\n onNone: () => {\n skipped.push(row.__key)\n return false\n },\n }),\n )\n\n // Each row judged alone: a refused row is one more that could not be told,\n // not a reason to answer nothing about the others.\n const judged = evaluateEach(reaching ? orderOnly(body) : body, encoded, admitted, {\n operators: options.operators,\n context: options.context,\n })\n const keyOf = (row: Row) => (row as { readonly __key: EntityKey }).__key\n\n return { matched: judged.matched.map(keyOf), skipped: [...skipped, ...judged.refused.map(keyOf)] }\n}\n\nconst reachingBodies = new WeakMap<AnyQuery, boolean>()\n\n/** Whether a body's predicates read other rows: a path, an exists, a path from the viewer. */\nconst reachesOtherRows = (body: AnyQuery): boolean => {\n const known = reachingBodies.get(body)\n if (known !== undefined) return known\n const reaching = Relational.dependencies(body).reaches.length > 0\n reachingBodies.set(body, reaching)\n return reaching\n}\n\nconst ordersOf = new WeakMap<AnyQuery, AnyQuery>()\n\n/** `body`'s order without its predicates, to put rows it admitted in order. */\nconst orderOnly = (body: AnyQuery): AnyQuery => {\n const known = ordersOf.get(body)\n if (known !== undefined) return known\n const ordered = Relational.from(body.entity).pipe(Relational.orderBy(...body.orderBy))\n ordersOf.set(body, ordered)\n return ordered\n}\n\n/**\n * Whether the held row under `key` is in `body`'s result, judged over the\n * whole store: none where the answer rests on what the store lacks.\n */\nconst judgedOver = (\n holder: StoreHolder,\n body: AnyQuery,\n encoded: Readonly<Record<string, unknown>>,\n key: EntityKey,\n prefix: string,\n options: MatchingOptions,\n): Option.Option<boolean> =>\n Option.flatMap(holder.rows.row(body.entity, key.slice(prefix.length)), row =>\n Judge.match(\n Judge.query(body, encoded, row, {\n operators: options.operators,\n context: options.context,\n rows: holder.rows,\n }),\n { onKnown: Option.some, onUnknown: () => Option.none() },\n ),\n )\n\n/**\n * Whether one row belongs to a query, as far as the client can tell.\n *\n * Three answers, and the third is the one that makes this usable. `'unknown'`\n * is a row the store never fetched, or holds without a field the body reads —\n * a caller that collapses it into `'no'` has turned \"I could not tell\" into an\n * answer, which is the mistake this whole module is shaped against.\n *\n * **Membership is decidable where position is not.** Whether a row satisfies\n * `eq`, `isNull` or `contains` needs no collation: equality on text is exact\n * and `contains` folds ASCII, both stated in §6.0.1. *Where* a row sorts needs\n * text collation, which is the backend's and deliberately outside the\n * conformant subset — so this answers the half that is always answerable, and\n * ordering stays the server's until a query can declare its collation.\n */\nexport type Belongs = 'yes' | 'no' | 'unknown'\n\n/**\n * Takes the input **encoded**, because its one caller holds it that way: a\n * connection identity carries the canonical encoded input, and making it decode\n * only for this to encode again would be a round trip that can fail.\n *\n * A decoded-input sibling was written first and deleted: nothing called it. The\n * asymmetry with `matching`, which takes a decoded input, is the shape of the\n * two real callers rather than an oversight.\n */\nexport const belongsEncoded = <Name extends string, Input>(\n store: EntityStore,\n descriptor: QueryDescriptor<Name, Input, unknown>,\n encoded: Readonly<Record<string, unknown>>,\n key: EntityKey,\n operators?: MemoryOperators | undefined,\n context?: ExprContext | undefined,\n): Belongs => {\n const entry = store[key]\n // Never fetched: nothing to judge, and silence is the honest answer.\n if (entry === undefined) return 'unknown'\n // Known absent. Not \"does not match\" — but it definitely does not belong in\n // a list, which is what a caller is asking.\n if (entry.tombstone) return 'no'\n\n // A value the store itself calls outdated cannot settle a question about new\n // information. `matching` still judges on a stale value — that is what is on\n // screen — but this is used to *decide*, and deciding \"does not belong\" from\n // a field the store says may be behind would suppress a live insert on the\n // strength of the very fact the insert contradicts.\n const body = descriptor.body\n if (body !== undefined && decidableOn(body).some(field => entry.stale.has(field))) {\n return 'unknown'\n }\n\n let judged: Judged\n try {\n judged = matchingEncoded(store, descriptor, encoded, { among: [key], operators, context })\n } catch (error) {\n // A body the reference interpreter refuses for this input (text holding\n // NUL) cannot settle membership; the server refuses it too.\n if (error instanceof QueryEvaluateError) return 'unknown'\n throw error\n }\n if (judged.matched.includes(key)) return 'yes'\n // Held, but missing a field the body reads.\n if (judged.skipped.includes(key)) return 'unknown'\n // Judged and did not match — or is a row of another Entity entirely, which\n // cannot belong to a connection over this one.\n return 'no'\n}\n"],"mappings":"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"}
|