@foir/sdk 0.8.0 → 0.10.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/CHANGELOG.md +550 -28
- package/README.md +111 -0
- package/dist/auth/auth.d.ts +72 -1
- package/dist/auth/hosted.d.ts +93 -4
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/operations.d.ts +84 -0
- package/dist/auth/ops.d.ts +10 -0
- package/dist/auth.js +138 -19
- package/dist/client.js +63 -6
- package/dist/codegen/block-registry.d.ts +1 -1
- package/dist/codegen/block-scaffold.d.ts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/item-registry.d.ts +1 -1
- package/dist/codegen/item-scaffold.d.ts +1 -1
- package/dist/content/content-renderer.d.ts +4 -5
- package/dist/content/index.d.ts +1 -1
- package/dist/content/richtext.d.ts +1 -1
- package/dist/data/builtins.d.ts +131 -12
- package/dist/data/client.d.ts +82 -1
- package/dist/data/decimal.d.ts +212 -0
- package/dist/data/document.d.ts +1 -1
- package/dist/data/index.d.ts +11 -9
- package/dist/data/manifest.d.ts +33 -6
- package/dist/data/schema.d.ts +2 -2
- package/dist/data/subscriptions.d.ts +36 -10
- package/dist/data/transport.d.ts +120 -3
- package/dist/data.js +377 -16
- package/dist/graphql/cdn-route.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/live/coalesce.d.ts +57 -0
- package/dist/live/drift.d.ts +59 -0
- package/dist/live/events.d.ts +13 -0
- package/dist/live/headers.d.ts +53 -0
- package/dist/live/index.d.ts +54 -0
- package/dist/live/keys.d.ts +72 -0
- package/dist/live/manager.d.ts +77 -0
- package/dist/live/react/index.d.ts +143 -0
- package/dist/live/reconcile.d.ts +82 -0
- package/dist/live/resync.d.ts +59 -0
- package/dist/live/store.d.ts +134 -0
- package/dist/live/tagIndex.d.ts +29 -0
- package/dist/live/tanstack.d.ts +127 -0
- package/dist/live/writetoken.d.ts +86 -0
- package/dist/live-react.js +395 -0
- package/dist/live.js +845 -0
- package/dist/next/cacheTags.d.ts +245 -0
- package/dist/next/index.d.ts +7 -0
- package/dist/next.js +179 -6
- package/dist/push.js +81 -8
- package/dist/react.js +76 -5
- package/dist/realtime/index.d.ts +4 -5
- package/dist/server/events-client.d.ts +54 -4
- package/dist/server/index.d.ts +3 -2
- package/dist/server/require-scoped-token.d.ts +1 -1
- package/dist/server.js +454 -2
- package/dist/transport/foir-http.d.ts +113 -10
- package/package.json +10 -2
package/dist/live.js
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
// src/data/decimal.ts
|
|
2
|
+
var DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
3
|
+
var MAX_SCALE = 1e3;
|
|
4
|
+
function pow10(n) {
|
|
5
|
+
return 10n ** BigInt(n);
|
|
6
|
+
}
|
|
7
|
+
function divRound(numerator, denominator, rounding) {
|
|
8
|
+
if (denominator === 0n) throw new RangeError("FoirDecimal: division by zero");
|
|
9
|
+
const negative = numerator < 0n !== denominator < 0n;
|
|
10
|
+
const n = numerator < 0n ? -numerator : numerator;
|
|
11
|
+
const d = denominator < 0n ? -denominator : denominator;
|
|
12
|
+
let quotient = n / d;
|
|
13
|
+
const remainder = n % d;
|
|
14
|
+
if (remainder !== 0n && rounding !== "down") {
|
|
15
|
+
const doubled = remainder * 2n;
|
|
16
|
+
if (doubled > d) {
|
|
17
|
+
quotient += 1n;
|
|
18
|
+
} else if (doubled === d) {
|
|
19
|
+
if (rounding === "half-up" || quotient % 2n === 1n) quotient += 1n;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return negative ? -quotient : quotient;
|
|
23
|
+
}
|
|
24
|
+
function parse(input) {
|
|
25
|
+
const text = input.trim();
|
|
26
|
+
if (!DECIMAL_PATTERN.test(text)) {
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
`FoirDecimal: ${JSON.stringify(input)} is not a decimal. Expected a numeric string such as "19.99".`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const [mantissa, exponentText] = text.split(/[eE]/);
|
|
32
|
+
const exponent = exponentText ? Number(exponentText) : 0;
|
|
33
|
+
const negative = mantissa.startsWith("-");
|
|
34
|
+
const unsigned = negative || mantissa.startsWith("+") ? mantissa.slice(1) : mantissa;
|
|
35
|
+
const [integerPart = "", fractionPart = ""] = unsigned.split(".");
|
|
36
|
+
let units = BigInt(`${integerPart || "0"}${fractionPart}`);
|
|
37
|
+
let scale = fractionPart.length - exponent;
|
|
38
|
+
if (scale < 0) {
|
|
39
|
+
units *= pow10(-scale);
|
|
40
|
+
scale = 0;
|
|
41
|
+
}
|
|
42
|
+
if (scale > MAX_SCALE) {
|
|
43
|
+
throw new RangeError(`FoirDecimal: scale ${scale} exceeds the maximum of ${MAX_SCALE}`);
|
|
44
|
+
}
|
|
45
|
+
return { units: negative ? -units : units, scale };
|
|
46
|
+
}
|
|
47
|
+
var FoirDecimal = class _FoirDecimal {
|
|
48
|
+
// Not `readonly` only so `#of` can populate a fresh instance without going
|
|
49
|
+
// back through the string parser. Never mutated after construction, and
|
|
50
|
+
// `#`-private so immutability is not part of the public contract to keep.
|
|
51
|
+
#units;
|
|
52
|
+
#scale;
|
|
53
|
+
constructor(value) {
|
|
54
|
+
if (value instanceof _FoirDecimal) {
|
|
55
|
+
this.#units = value.#units;
|
|
56
|
+
this.#scale = value.#scale;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value === "bigint") {
|
|
60
|
+
this.#units = value;
|
|
61
|
+
this.#scale = 0;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value !== "string") {
|
|
65
|
+
throw new TypeError(
|
|
66
|
+
`FoirDecimal: refusing to construct from ${typeof value}. A JavaScript number is a float and may already have lost precision \u2014 pass the string, or use FoirDecimal.fromNumber() to accept that loss explicitly.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const { units, scale } = parse(value);
|
|
70
|
+
this.#units = units;
|
|
71
|
+
this.#scale = scale;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build a decimal from a JavaScript number, accepting the precision that
|
|
75
|
+
* number may already have lost.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately not the constructor. It is for interop — a value read from a
|
|
78
|
+
* third-party JSON API that emitted a float — and naming it at every call
|
|
79
|
+
* site is the point. `fromNumber(0.1 + 0.2)` gives `0.30000000000000004`,
|
|
80
|
+
* because that is genuinely the value it was handed.
|
|
81
|
+
*/
|
|
82
|
+
static fromNumber(value) {
|
|
83
|
+
if (!Number.isFinite(value)) {
|
|
84
|
+
throw new RangeError(`FoirDecimal: ${value} is not a finite number`);
|
|
85
|
+
}
|
|
86
|
+
return new _FoirDecimal(value.toPrecision(17));
|
|
87
|
+
}
|
|
88
|
+
/** `true` if `value` parses as a decimal. Does not throw. */
|
|
89
|
+
static isDecimalString(value) {
|
|
90
|
+
return typeof value === "string" && DECIMAL_PATTERN.test(value.trim());
|
|
91
|
+
}
|
|
92
|
+
/** Decimal places carried by this value. `19.90` has scale 2. */
|
|
93
|
+
get scale() {
|
|
94
|
+
return this.#scale;
|
|
95
|
+
}
|
|
96
|
+
/** Re-express at `scale`, rounding away any digits that no longer fit. */
|
|
97
|
+
rescale(scale, rounding = "half-up") {
|
|
98
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
99
|
+
throw new RangeError(`FoirDecimal: scale must be an integer in 0..${MAX_SCALE}, got ${scale}`);
|
|
100
|
+
}
|
|
101
|
+
if (scale === this.#scale) return this;
|
|
102
|
+
const units = scale > this.#scale ? this.#units * pow10(scale - this.#scale) : divRound(this.#units, pow10(this.#scale - scale), rounding);
|
|
103
|
+
return _FoirDecimal.#of(units, scale);
|
|
104
|
+
}
|
|
105
|
+
add(other) {
|
|
106
|
+
const [a, b, scale] = _FoirDecimal.#align(this, other);
|
|
107
|
+
return _FoirDecimal.#of(a + b, scale);
|
|
108
|
+
}
|
|
109
|
+
sub(other) {
|
|
110
|
+
const [a, b, scale] = _FoirDecimal.#align(this, other);
|
|
111
|
+
return _FoirDecimal.#of(a - b, scale);
|
|
112
|
+
}
|
|
113
|
+
/** Exact: scales add, so `1.10 × 1.10` is `1.2100` rather than `1.21`. */
|
|
114
|
+
mul(other) {
|
|
115
|
+
const rhs = new _FoirDecimal(other);
|
|
116
|
+
return _FoirDecimal.#of(this.#units * rhs.#units, this.#scale + rhs.#scale);
|
|
117
|
+
}
|
|
118
|
+
/** The only inexact operation — see {@link FoirDecimalDivOptions}. */
|
|
119
|
+
div(other, options = {}) {
|
|
120
|
+
const { scale = 20, rounding = "half-even" } = options;
|
|
121
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
122
|
+
throw new RangeError(`FoirDecimal: scale must be an integer in 0..${MAX_SCALE}, got ${scale}`);
|
|
123
|
+
}
|
|
124
|
+
const rhs = new _FoirDecimal(other);
|
|
125
|
+
const numerator = this.#units * pow10(rhs.#scale + scale);
|
|
126
|
+
const denominator = rhs.#units * pow10(this.#scale);
|
|
127
|
+
return _FoirDecimal.#of(divRound(numerator, denominator, rounding), scale);
|
|
128
|
+
}
|
|
129
|
+
abs() {
|
|
130
|
+
return this.#units < 0n ? _FoirDecimal.#of(-this.#units, this.#scale) : this;
|
|
131
|
+
}
|
|
132
|
+
neg() {
|
|
133
|
+
return _FoirDecimal.#of(-this.#units, this.#scale);
|
|
134
|
+
}
|
|
135
|
+
isZero() {
|
|
136
|
+
return this.#units === 0n;
|
|
137
|
+
}
|
|
138
|
+
isNegative() {
|
|
139
|
+
return this.#units < 0n;
|
|
140
|
+
}
|
|
141
|
+
/** `-1`, `0` or `1`. Use this rather than `<`, which throws. */
|
|
142
|
+
cmp(other) {
|
|
143
|
+
const [a, b] = _FoirDecimal.#align(this, other);
|
|
144
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
145
|
+
}
|
|
146
|
+
/** Numeric equality: `1.0` equals `1.00`. Scale is display, not identity. */
|
|
147
|
+
equals(other) {
|
|
148
|
+
return this.cmp(other) === 0;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Fixed-place string, rounding `half-up` by default because this is the
|
|
152
|
+
* display operation and that is what a person means by "to 2dp".
|
|
153
|
+
*/
|
|
154
|
+
toFixed(places, rounding = "half-up") {
|
|
155
|
+
return this.rescale(places, rounding).toString();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The canonical wire form, preserving scale. Never exponent notation, so the
|
|
159
|
+
* output always parses back to an equal value.
|
|
160
|
+
*/
|
|
161
|
+
toString() {
|
|
162
|
+
const negative = this.#units < 0n;
|
|
163
|
+
const digits = (negative ? -this.#units : this.#units).toString();
|
|
164
|
+
if (this.#scale === 0) return negative ? `-${digits}` : digits;
|
|
165
|
+
const padded = digits.padStart(this.#scale + 1, "0");
|
|
166
|
+
const integerPart = padded.slice(0, padded.length - this.#scale);
|
|
167
|
+
const fractionPart = padded.slice(padded.length - this.#scale);
|
|
168
|
+
return `${negative ? "-" : ""}${integerPart}.${fractionPart}`;
|
|
169
|
+
}
|
|
170
|
+
/** `JSON.stringify` emits the canonical string, not an object. Pinned by test. */
|
|
171
|
+
toJSON() {
|
|
172
|
+
return this.toString();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Explicitly lossy — this is the boundary back into float, and naming it is
|
|
176
|
+
* the point. Fine for a chart axis; never for money you will add up.
|
|
177
|
+
*/
|
|
178
|
+
toNumber() {
|
|
179
|
+
return Number(this.toString());
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Answers the `'string'` hint so template literals work, and throws on every
|
|
183
|
+
* other hint so `+`, `<` and `Number()` fail loudly instead of silently
|
|
184
|
+
* concatenating or truncating.
|
|
185
|
+
*/
|
|
186
|
+
[Symbol.toPrimitive](hint) {
|
|
187
|
+
if (hint === "string") return this.toString();
|
|
188
|
+
throw new TypeError(
|
|
189
|
+
hint === "number" ? "FoirDecimal: cannot be coerced to a number implicitly. Use .toNumber() to accept the precision loss, or .cmp() to compare." : "FoirDecimal: cannot be coerced implicitly. Use .add() / .sub() / .mul() / .div() for arithmetic, .cmp() to compare, or .toString() for text."
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
get [Symbol.toStringTag]() {
|
|
193
|
+
return "FoirDecimal";
|
|
194
|
+
}
|
|
195
|
+
/** Skip parsing when the units are already known to be well-formed. */
|
|
196
|
+
static #of(units, scale) {
|
|
197
|
+
const instance = new _FoirDecimal(0n);
|
|
198
|
+
instance.#units = units;
|
|
199
|
+
instance.#scale = scale;
|
|
200
|
+
return instance;
|
|
201
|
+
}
|
|
202
|
+
/** Bring two values to a common scale so their units can be compared. */
|
|
203
|
+
static #align(left, right) {
|
|
204
|
+
const rhs = new _FoirDecimal(right);
|
|
205
|
+
const scale = Math.max(left.#scale, rhs.#scale);
|
|
206
|
+
return [
|
|
207
|
+
left.#units * pow10(scale - left.#scale),
|
|
208
|
+
rhs.#units * pow10(scale - rhs.#scale),
|
|
209
|
+
scale
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/live/keys.ts
|
|
215
|
+
function foirDecimalKey(value) {
|
|
216
|
+
const text = value.toString();
|
|
217
|
+
if (!text.includes(".")) return text;
|
|
218
|
+
const trimmed = text.replace(/0+$/, "").replace(/\.$/, "");
|
|
219
|
+
return trimmed === "-0" || trimmed === "" ? "0" : trimmed;
|
|
220
|
+
}
|
|
221
|
+
function foirStableKey(value) {
|
|
222
|
+
if (value === null) return "null";
|
|
223
|
+
if (value === void 0) return "undef";
|
|
224
|
+
if (value instanceof FoirDecimal) return `d:${foirDecimalKey(value)}`;
|
|
225
|
+
if (value instanceof Date) return `t:${value.toISOString()}`;
|
|
226
|
+
if (Array.isArray(value)) return `[${value.map(foirStableKey).join(",")}]`;
|
|
227
|
+
switch (typeof value) {
|
|
228
|
+
case "string":
|
|
229
|
+
return `s:${JSON.stringify(value)}`;
|
|
230
|
+
case "number":
|
|
231
|
+
return `n:${Object.is(value, -0) ? "0" : String(value)}`;
|
|
232
|
+
case "bigint":
|
|
233
|
+
return `n:${value.toString()}`;
|
|
234
|
+
case "boolean":
|
|
235
|
+
return `b:${value ? "1" : "0"}`;
|
|
236
|
+
case "object": {
|
|
237
|
+
const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${foirStableKey(value[k])}`);
|
|
238
|
+
return `{${entries.join(",")}}`;
|
|
239
|
+
}
|
|
240
|
+
default:
|
|
241
|
+
throw new TypeError(
|
|
242
|
+
`[foir] a query argument of type ${typeof value} cannot be part of a cache key`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function callTarget(call) {
|
|
247
|
+
return "model" in call ? `m:${call.model}` : `ns:${call.namespace}`;
|
|
248
|
+
}
|
|
249
|
+
function foirQueryKey(call) {
|
|
250
|
+
const parts = [
|
|
251
|
+
callTarget(call),
|
|
252
|
+
`op:${call.op}`,
|
|
253
|
+
`args:${foirStableKey(call.args ?? {})}`
|
|
254
|
+
];
|
|
255
|
+
if (call.include !== void 0) parts.push(`inc:${foirStableKey(call.include)}`);
|
|
256
|
+
if (call.omit !== void 0) parts.push(`omit:${foirStableKey([...call.omit].sort())}`);
|
|
257
|
+
if (call.pick !== void 0) parts.push(`pick:${foirStableKey([...call.pick].sort())}`);
|
|
258
|
+
if (call.depth !== void 0) parts.push(`depth:${call.depth}`);
|
|
259
|
+
return parts.join("|");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/live/headers.ts
|
|
263
|
+
var FOIR_CACHE_TAGS_HEADER = "X-Foir-Cache-Tags";
|
|
264
|
+
var FOIR_SURROGATE_KEY_HEADER = "Surrogate-Key";
|
|
265
|
+
var FOIR_PROJECT_SEQ_HEADER = "X-Foir-Project-Seq";
|
|
266
|
+
function splitTags(raw) {
|
|
267
|
+
if (!raw) return [];
|
|
268
|
+
const seen = /* @__PURE__ */ new Set();
|
|
269
|
+
const out = [];
|
|
270
|
+
for (const tag of raw.split(/\s+/)) {
|
|
271
|
+
if (!tag || seen.has(tag)) continue;
|
|
272
|
+
seen.add(tag);
|
|
273
|
+
out.push(tag);
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
function readFoirLiveHeaders(headers) {
|
|
278
|
+
const surrogate = splitTags(headers.get(FOIR_SURROGATE_KEY_HEADER));
|
|
279
|
+
const tags = surrogate.length > 0 ? surrogate : splitTags(headers.get(FOIR_CACHE_TAGS_HEADER));
|
|
280
|
+
const raw = headers.get(FOIR_PROJECT_SEQ_HEADER);
|
|
281
|
+
let seq = null;
|
|
282
|
+
if (raw) {
|
|
283
|
+
const parsed = Number(raw);
|
|
284
|
+
if (Number.isSafeInteger(parsed) && parsed > 0) seq = parsed;
|
|
285
|
+
}
|
|
286
|
+
return { tags, seq };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/live/writetoken.ts
|
|
290
|
+
var FOIR_WRITE_TOKEN_HEADER = "X-Foir-Write-Token";
|
|
291
|
+
function readFoirWriteToken(headers) {
|
|
292
|
+
const raw = headers.get(FOIR_WRITE_TOKEN_HEADER);
|
|
293
|
+
if (typeof raw !== "string") return null;
|
|
294
|
+
const token = raw.trim();
|
|
295
|
+
return token === "" ? null : token;
|
|
296
|
+
}
|
|
297
|
+
function foirWriteTokenHeaders(token) {
|
|
298
|
+
if (typeof token !== "string") return {};
|
|
299
|
+
const trimmed = token.trim();
|
|
300
|
+
return trimmed === "" ? {} : { [FOIR_WRITE_TOKEN_HEADER]: trimmed };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/live/events.ts
|
|
304
|
+
var RECORD_TAG_PREFIX = "rec:";
|
|
305
|
+
var CACHED_CHANNEL = "published";
|
|
306
|
+
function foirRecordTag(recordId) {
|
|
307
|
+
return `${RECORD_TAG_PREFIX}${recordId}`;
|
|
308
|
+
}
|
|
309
|
+
function foirModelTag(modelKey) {
|
|
310
|
+
return `model:${modelKey}:${CACHED_CHANNEL}`;
|
|
311
|
+
}
|
|
312
|
+
function foirTagsForRecordChange(event) {
|
|
313
|
+
const record = foirRecordTag(event.recordId);
|
|
314
|
+
if (event.action === "updated") return [record];
|
|
315
|
+
return [record, foirModelTag(event.modelKey)];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/live/tagIndex.ts
|
|
319
|
+
function createFoirTagIndex() {
|
|
320
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
321
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
322
|
+
function unindex(key) {
|
|
323
|
+
const previous = byKey.get(key);
|
|
324
|
+
if (!previous) return;
|
|
325
|
+
for (const tag of previous) {
|
|
326
|
+
const keys = byTag.get(tag);
|
|
327
|
+
if (!keys) continue;
|
|
328
|
+
keys.delete(key);
|
|
329
|
+
if (keys.size === 0) byTag.delete(tag);
|
|
330
|
+
}
|
|
331
|
+
byKey.delete(key);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
set(key, tags) {
|
|
335
|
+
unindex(key);
|
|
336
|
+
const unique = [...new Set(tags)];
|
|
337
|
+
if (unique.length === 0) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
byKey.set(key, unique);
|
|
341
|
+
for (const tag of unique) {
|
|
342
|
+
let keys = byTag.get(tag);
|
|
343
|
+
if (!keys) {
|
|
344
|
+
keys = /* @__PURE__ */ new Set();
|
|
345
|
+
byTag.set(tag, keys);
|
|
346
|
+
}
|
|
347
|
+
keys.add(key);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
remove: unindex,
|
|
351
|
+
tagsFor(key) {
|
|
352
|
+
return byKey.get(key) ?? [];
|
|
353
|
+
},
|
|
354
|
+
keysForTags(tags) {
|
|
355
|
+
const out = /* @__PURE__ */ new Set();
|
|
356
|
+
for (const tag of tags) {
|
|
357
|
+
const keys = byTag.get(tag);
|
|
358
|
+
if (!keys) continue;
|
|
359
|
+
for (const key of keys) out.add(key);
|
|
360
|
+
}
|
|
361
|
+
return [...out];
|
|
362
|
+
},
|
|
363
|
+
clear() {
|
|
364
|
+
byKey.clear();
|
|
365
|
+
byTag.clear();
|
|
366
|
+
},
|
|
367
|
+
get size() {
|
|
368
|
+
return byKey.size;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/live/store.ts
|
|
374
|
+
function createFoirLiveStore(options = {}) {
|
|
375
|
+
const now = options.now ?? (() => Date.now());
|
|
376
|
+
const entries = /* @__PURE__ */ new Map();
|
|
377
|
+
const index = createFoirTagIndex();
|
|
378
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
379
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
380
|
+
const appliedSeq = /* @__PURE__ */ new Map();
|
|
381
|
+
let epoch = 0;
|
|
382
|
+
function announce(invalidation) {
|
|
383
|
+
for (const listener of listeners) listener(invalidation);
|
|
384
|
+
}
|
|
385
|
+
function dropKey(key) {
|
|
386
|
+
entries.delete(key);
|
|
387
|
+
index.remove(key);
|
|
388
|
+
}
|
|
389
|
+
function writeAt(key, write, issuedAt) {
|
|
390
|
+
if (issuedAt !== epoch) return false;
|
|
391
|
+
const existing = entries.get(key);
|
|
392
|
+
const seq = write.seq ?? null;
|
|
393
|
+
if (existing && existing.seq !== null && seq !== null && seq < existing.seq) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
const tags = write.tags ?? [];
|
|
397
|
+
entries.set(key, {
|
|
398
|
+
payload: write.payload,
|
|
399
|
+
tags,
|
|
400
|
+
seq,
|
|
401
|
+
omitted: write.omit ? [...write.omit] : [],
|
|
402
|
+
storedAt: now()
|
|
403
|
+
});
|
|
404
|
+
index.set(key, tags);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
read(key, readOptions) {
|
|
409
|
+
const entry = entries.get(key);
|
|
410
|
+
if (!entry) return void 0;
|
|
411
|
+
const needs = readOptions?.needs;
|
|
412
|
+
if (needs && needs.length > 0 && entry.omitted.length > 0) {
|
|
413
|
+
for (const need of needs) {
|
|
414
|
+
if (entry.omitted.includes(need)) return void 0;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return entry;
|
|
418
|
+
},
|
|
419
|
+
write(key, write) {
|
|
420
|
+
return writeAt(key, write, epoch);
|
|
421
|
+
},
|
|
422
|
+
drop: dropKey,
|
|
423
|
+
dedupe(key, run) {
|
|
424
|
+
const running = inFlight.get(key);
|
|
425
|
+
if (running) return running;
|
|
426
|
+
const issuedAt = epoch;
|
|
427
|
+
const fence = {
|
|
428
|
+
write: (write) => writeAt(key, write, issuedAt),
|
|
429
|
+
get current() {
|
|
430
|
+
return issuedAt === epoch;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
const promise = (async () => run(fence))().finally(() => {
|
|
434
|
+
if (inFlight.get(key) === promise) inFlight.delete(key);
|
|
435
|
+
});
|
|
436
|
+
inFlight.set(key, promise);
|
|
437
|
+
return promise;
|
|
438
|
+
},
|
|
439
|
+
applyRecordChange(event) {
|
|
440
|
+
const tags = foirTagsForRecordChange(event);
|
|
441
|
+
const seq = typeof event.seq === "number" ? event.seq : null;
|
|
442
|
+
if (seq !== null) {
|
|
443
|
+
const applied = appliedSeq.get(event.recordId);
|
|
444
|
+
if (applied !== void 0 && seq <= applied) {
|
|
445
|
+
return { keys: [], tags, ignored: true };
|
|
446
|
+
}
|
|
447
|
+
appliedSeq.set(event.recordId, seq);
|
|
448
|
+
}
|
|
449
|
+
const keys = index.keysForTags(tags);
|
|
450
|
+
for (const key of keys) dropKey(key);
|
|
451
|
+
const invalidation = { keys, tags, ignored: false };
|
|
452
|
+
if (keys.length > 0) announce(invalidation);
|
|
453
|
+
return invalidation;
|
|
454
|
+
},
|
|
455
|
+
resync() {
|
|
456
|
+
epoch += 1;
|
|
457
|
+
const keys = [...entries.keys()];
|
|
458
|
+
entries.clear();
|
|
459
|
+
index.clear();
|
|
460
|
+
appliedSeq.clear();
|
|
461
|
+
announce({ keys, tags: [], ignored: false });
|
|
462
|
+
},
|
|
463
|
+
onInvalidate(listener) {
|
|
464
|
+
listeners.add(listener);
|
|
465
|
+
return () => listeners.delete(listener);
|
|
466
|
+
},
|
|
467
|
+
get size() {
|
|
468
|
+
return entries.size;
|
|
469
|
+
},
|
|
470
|
+
tags: index
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/live/resync.ts
|
|
475
|
+
var FOIR_RESYNC_ERROR_CODES = ["cursor_expired", "backlog_too_large"];
|
|
476
|
+
function foirRequiresResync(error) {
|
|
477
|
+
const code = error.code;
|
|
478
|
+
return typeof code === "string" && FOIR_RESYNC_ERROR_CODES.includes(code);
|
|
479
|
+
}
|
|
480
|
+
function foirResumeCursor(error) {
|
|
481
|
+
const resumeFrom = error.graphQLErrors?.[0]?.extensions?.resumeFrom;
|
|
482
|
+
return typeof resumeFrom === "number" && Number.isSafeInteger(resumeFrom) && resumeFrom > 0 ? resumeFrom : null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/live/reconcile.ts
|
|
486
|
+
var RECORD_TAG_PREFIX2 = "rec:";
|
|
487
|
+
function foirHeldRecordIds(index, modelKey) {
|
|
488
|
+
const ids2 = /* @__PURE__ */ new Set();
|
|
489
|
+
for (const key of index.keysForTags([foirModelTag(modelKey)])) {
|
|
490
|
+
for (const tag of index.tagsFor(key)) {
|
|
491
|
+
if (tag.startsWith(RECORD_TAG_PREFIX2)) ids2.add(tag.slice(RECORD_TAG_PREFIX2.length));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return [...ids2];
|
|
495
|
+
}
|
|
496
|
+
var DEFAULT_BATCH_SIZE = 200;
|
|
497
|
+
function createFoirReconciler(options) {
|
|
498
|
+
const batchSize = options.batchSize && options.batchSize > 0 ? options.batchSize : DEFAULT_BATCH_SIZE;
|
|
499
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
500
|
+
async function run(modelKey) {
|
|
501
|
+
const held = [...new Set(options.heldIds?.(modelKey) ?? [])];
|
|
502
|
+
if (held.length === 0) return [];
|
|
503
|
+
const visible = /* @__PURE__ */ new Set();
|
|
504
|
+
for (let i = 0; i < held.length; i += batchSize) {
|
|
505
|
+
const batch = held.slice(i, i + batchSize);
|
|
506
|
+
let answer;
|
|
507
|
+
try {
|
|
508
|
+
answer = await options.resolve(modelKey, batch);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
options.onError?.(error, modelKey);
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
for (const id of answer) visible.add(id);
|
|
514
|
+
}
|
|
515
|
+
const gone = held.filter((id) => !visible.has(id));
|
|
516
|
+
if (gone.length > 0) options.onGone(modelKey, gone);
|
|
517
|
+
return gone;
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
reconcile(modelKey) {
|
|
521
|
+
const existing = inFlight.get(modelKey);
|
|
522
|
+
if (existing) return existing;
|
|
523
|
+
const promise = run(modelKey).finally(() => {
|
|
524
|
+
inFlight.delete(modelKey);
|
|
525
|
+
});
|
|
526
|
+
inFlight.set(modelKey, promise);
|
|
527
|
+
return promise;
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function foirTagsForGoneRecords(modelKey, goneIds) {
|
|
532
|
+
if (goneIds.length === 0) return [];
|
|
533
|
+
return [...goneIds.map(foirRecordTag), foirModelTag(modelKey)];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/live/manager.ts
|
|
537
|
+
function createFoirLiveManager(options) {
|
|
538
|
+
const watches = /* @__PURE__ */ new Map();
|
|
539
|
+
function open(modelKey, watch) {
|
|
540
|
+
const after = watch.cursor ?? void 0;
|
|
541
|
+
watch.subscription = options.subscribe(
|
|
542
|
+
modelKey,
|
|
543
|
+
after === void 0 ? {} : { after },
|
|
544
|
+
{
|
|
545
|
+
onData: (event) => {
|
|
546
|
+
if (typeof event.seq === "number" && (watch.cursor === null || event.seq > watch.cursor)) {
|
|
547
|
+
watch.cursor = event.seq;
|
|
548
|
+
}
|
|
549
|
+
options.onChange(event);
|
|
550
|
+
},
|
|
551
|
+
onError: (error) => {
|
|
552
|
+
if (foirRequiresResync(error)) {
|
|
553
|
+
watch.cursor = null;
|
|
554
|
+
options.onResync?.(error, modelKey);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
options.onError?.(error, modelKey);
|
|
558
|
+
},
|
|
559
|
+
onReconnect: () => {
|
|
560
|
+
resume(modelKey);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
function resume(modelKey) {
|
|
566
|
+
const watch = watches.get(modelKey);
|
|
567
|
+
if (!watch || watch.count === 0 || watch.resuming) return;
|
|
568
|
+
watch.resuming = true;
|
|
569
|
+
try {
|
|
570
|
+
watch.subscription?.unsubscribe();
|
|
571
|
+
watch.subscription = null;
|
|
572
|
+
open(modelKey, watch);
|
|
573
|
+
} finally {
|
|
574
|
+
watch.resuming = false;
|
|
575
|
+
}
|
|
576
|
+
options.reconcile?.(modelKey);
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
retain(modelKey) {
|
|
580
|
+
let watch = watches.get(modelKey);
|
|
581
|
+
if (!watch) {
|
|
582
|
+
watch = { count: 0, subscription: null, cursor: null, resuming: false };
|
|
583
|
+
watches.set(modelKey, watch);
|
|
584
|
+
}
|
|
585
|
+
watch.count += 1;
|
|
586
|
+
if (watch.count === 1) open(modelKey, watch);
|
|
587
|
+
let released = false;
|
|
588
|
+
return () => {
|
|
589
|
+
if (released) return;
|
|
590
|
+
released = true;
|
|
591
|
+
const current = watches.get(modelKey);
|
|
592
|
+
if (!current) return;
|
|
593
|
+
current.count -= 1;
|
|
594
|
+
if (current.count > 0) return;
|
|
595
|
+
current.subscription?.unsubscribe();
|
|
596
|
+
current.subscription = null;
|
|
597
|
+
};
|
|
598
|
+
},
|
|
599
|
+
cursorFor(modelKey) {
|
|
600
|
+
return watches.get(modelKey)?.cursor ?? null;
|
|
601
|
+
},
|
|
602
|
+
watchers(modelKey) {
|
|
603
|
+
return watches.get(modelKey)?.count ?? 0;
|
|
604
|
+
},
|
|
605
|
+
get watching() {
|
|
606
|
+
return [...watches.entries()].filter(([, w]) => w.subscription !== null).map(([k]) => k);
|
|
607
|
+
},
|
|
608
|
+
dispose() {
|
|
609
|
+
for (const watch of watches.values()) {
|
|
610
|
+
watch.subscription?.unsubscribe();
|
|
611
|
+
watch.subscription = null;
|
|
612
|
+
watch.count = 0;
|
|
613
|
+
}
|
|
614
|
+
watches.clear();
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/live/tanstack.ts
|
|
620
|
+
var FOIR_QUERY_KEY_ROOT = "foir";
|
|
621
|
+
function foirQueryKeys(call) {
|
|
622
|
+
const target = "model" in call ? call.model : call.namespace;
|
|
623
|
+
return [FOIR_QUERY_KEY_ROOT, target, call.op, foirQueryKey(call)];
|
|
624
|
+
}
|
|
625
|
+
function foirModelQueryKey(modelKey) {
|
|
626
|
+
return [FOIR_QUERY_KEY_ROOT, modelKey];
|
|
627
|
+
}
|
|
628
|
+
function applyFoirRecordChange(queryClient, event, options = {}) {
|
|
629
|
+
const mode = options.mode ?? "auto";
|
|
630
|
+
const idField = options.idField ?? "_id";
|
|
631
|
+
const effect = {
|
|
632
|
+
invalidated: [],
|
|
633
|
+
removed: [],
|
|
634
|
+
created: event.action === "created",
|
|
635
|
+
totalCountStale: event.action === "created" || event.action === "deleted"
|
|
636
|
+
};
|
|
637
|
+
if (mode === "off" || mode === "notify") return effect;
|
|
638
|
+
const modelKey = foirModelQueryKey(event.modelKey);
|
|
639
|
+
if (event.action === "deleted") {
|
|
640
|
+
const removed = [];
|
|
641
|
+
queryClient.setQueriesData({ queryKey: modelKey }, (previous) => {
|
|
642
|
+
const next = withoutRecord(previous, event.recordId, idField);
|
|
643
|
+
return next.changed ? next.value : previous;
|
|
644
|
+
});
|
|
645
|
+
for (const [key, value] of queryClient.getQueriesData({ queryKey: modelKey })) {
|
|
646
|
+
if (!containsRecord(value, event.recordId, idField)) removed.push(key);
|
|
647
|
+
}
|
|
648
|
+
return { ...effect, removed };
|
|
649
|
+
}
|
|
650
|
+
if (event.action === "updated") {
|
|
651
|
+
const invalidated = [];
|
|
652
|
+
const invalidate = options.invalidate ?? ((key) => {
|
|
653
|
+
queryClient.invalidateQueries({ queryKey: key, exact: true });
|
|
654
|
+
});
|
|
655
|
+
for (const [key, value] of queryClient.getQueriesData({ queryKey: modelKey })) {
|
|
656
|
+
if (!containsRecord(value, event.recordId, idField)) continue;
|
|
657
|
+
invalidate(key);
|
|
658
|
+
invalidated.push(key);
|
|
659
|
+
}
|
|
660
|
+
return { ...effect, invalidated };
|
|
661
|
+
}
|
|
662
|
+
return effect;
|
|
663
|
+
}
|
|
664
|
+
function foirTagsForQueryInvalidation(event) {
|
|
665
|
+
return event.action === "updated" ? [foirRecordTag(event.recordId)] : [foirRecordTag(event.recordId), foirModelTag(event.modelKey)];
|
|
666
|
+
}
|
|
667
|
+
function ids(value, idField) {
|
|
668
|
+
const page = value;
|
|
669
|
+
if (!page || typeof page !== "object") return [];
|
|
670
|
+
const out = [];
|
|
671
|
+
for (const edge of page.edges ?? []) {
|
|
672
|
+
const id = edge?.node?.[idField];
|
|
673
|
+
if (typeof id === "string") out.push(id);
|
|
674
|
+
}
|
|
675
|
+
for (const node of page.nodes ?? []) {
|
|
676
|
+
const id = node?.[idField];
|
|
677
|
+
if (typeof id === "string") out.push(id);
|
|
678
|
+
}
|
|
679
|
+
const own = page[idField];
|
|
680
|
+
if (typeof own === "string") out.push(own);
|
|
681
|
+
return out;
|
|
682
|
+
}
|
|
683
|
+
function containsRecord(value, recordId, idField) {
|
|
684
|
+
return ids(value, idField).includes(recordId);
|
|
685
|
+
}
|
|
686
|
+
function withoutRecord(value, recordId, idField) {
|
|
687
|
+
const page = value;
|
|
688
|
+
if (!page || typeof page !== "object") return { value, changed: false };
|
|
689
|
+
let changed = false;
|
|
690
|
+
const next = { ...page };
|
|
691
|
+
if (Array.isArray(page.edges)) {
|
|
692
|
+
const edges = page.edges.filter(
|
|
693
|
+
(edge) => edge?.node?.[idField] !== recordId
|
|
694
|
+
);
|
|
695
|
+
if (edges.length !== page.edges.length) {
|
|
696
|
+
next.edges = edges;
|
|
697
|
+
changed = true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (Array.isArray(page.nodes)) {
|
|
701
|
+
const nodes = page.nodes.filter(
|
|
702
|
+
(node) => node?.[idField] !== recordId
|
|
703
|
+
);
|
|
704
|
+
if (nodes.length !== page.nodes.length) {
|
|
705
|
+
next.nodes = nodes;
|
|
706
|
+
changed = true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return changed ? { value: next, changed: true } : { value, changed: false };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/live/coalesce.ts
|
|
713
|
+
var DEFAULT_WINDOW_MS = 25;
|
|
714
|
+
var DEFAULT_MAX_CONCURRENT = 4;
|
|
715
|
+
function createFoirCoalescer(options) {
|
|
716
|
+
const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
|
|
717
|
+
const maxConcurrent = Math.max(1, options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT);
|
|
718
|
+
const timers = options.timers ?? {
|
|
719
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
720
|
+
clearTimeout: (handle) => clearTimeout(handle)
|
|
721
|
+
};
|
|
722
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
723
|
+
const running = /* @__PURE__ */ new Set();
|
|
724
|
+
const superseded = /* @__PURE__ */ new Set();
|
|
725
|
+
let timer = null;
|
|
726
|
+
let idle = [];
|
|
727
|
+
let disposed = false;
|
|
728
|
+
function settleIdle() {
|
|
729
|
+
if (waiting.size > 0 || running.size > 0 || timer !== null) return;
|
|
730
|
+
const waiters = idle;
|
|
731
|
+
idle = [];
|
|
732
|
+
for (const resolve of waiters) resolve();
|
|
733
|
+
}
|
|
734
|
+
function pump() {
|
|
735
|
+
while (running.size < maxConcurrent && waiting.size > 0) {
|
|
736
|
+
const key = waiting.values().next().value;
|
|
737
|
+
waiting.delete(key);
|
|
738
|
+
running.add(key);
|
|
739
|
+
void start(key);
|
|
740
|
+
}
|
|
741
|
+
settleIdle();
|
|
742
|
+
}
|
|
743
|
+
async function start(key) {
|
|
744
|
+
try {
|
|
745
|
+
await options.run(key);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
options.onError?.(error, key);
|
|
748
|
+
} finally {
|
|
749
|
+
running.delete(key);
|
|
750
|
+
if (superseded.delete(key) && !disposed) {
|
|
751
|
+
waiting.add(key);
|
|
752
|
+
}
|
|
753
|
+
pump();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function arm() {
|
|
757
|
+
if (timer !== null || disposed) return;
|
|
758
|
+
timer = timers.setTimeout(() => {
|
|
759
|
+
timer = null;
|
|
760
|
+
pump();
|
|
761
|
+
}, windowMs);
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
invalidate(key) {
|
|
765
|
+
if (disposed) return;
|
|
766
|
+
if (running.has(key)) {
|
|
767
|
+
superseded.add(key);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
waiting.add(key);
|
|
771
|
+
arm();
|
|
772
|
+
},
|
|
773
|
+
async flush() {
|
|
774
|
+
if (timer !== null) {
|
|
775
|
+
timers.clearTimeout(timer);
|
|
776
|
+
timer = null;
|
|
777
|
+
}
|
|
778
|
+
pump();
|
|
779
|
+
if (waiting.size === 0 && running.size === 0) return;
|
|
780
|
+
await new Promise((resolve) => idle.push(resolve));
|
|
781
|
+
},
|
|
782
|
+
get pending() {
|
|
783
|
+
return waiting.size;
|
|
784
|
+
},
|
|
785
|
+
get inFlight() {
|
|
786
|
+
return running.size;
|
|
787
|
+
},
|
|
788
|
+
dispose() {
|
|
789
|
+
disposed = true;
|
|
790
|
+
if (timer !== null) {
|
|
791
|
+
timers.clearTimeout(timer);
|
|
792
|
+
timer = null;
|
|
793
|
+
}
|
|
794
|
+
waiting.clear();
|
|
795
|
+
superseded.clear();
|
|
796
|
+
settleIdle();
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/live/drift.ts
|
|
802
|
+
function foirDriftIsActionable(drift) {
|
|
803
|
+
return Number.isFinite(drift.generated) && Number.isFinite(drift.server) && drift.generated !== drift.server;
|
|
804
|
+
}
|
|
805
|
+
function createFoirDriftHandler(options) {
|
|
806
|
+
return (drift) => {
|
|
807
|
+
if (!foirDriftIsActionable(drift)) {
|
|
808
|
+
options.onIgnored?.(drift);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
options.resync(drift);
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
export {
|
|
815
|
+
FOIR_CACHE_TAGS_HEADER,
|
|
816
|
+
FOIR_PROJECT_SEQ_HEADER,
|
|
817
|
+
FOIR_QUERY_KEY_ROOT,
|
|
818
|
+
FOIR_RESYNC_ERROR_CODES,
|
|
819
|
+
FOIR_SURROGATE_KEY_HEADER,
|
|
820
|
+
FOIR_WRITE_TOKEN_HEADER,
|
|
821
|
+
applyFoirRecordChange,
|
|
822
|
+
createFoirCoalescer,
|
|
823
|
+
createFoirDriftHandler,
|
|
824
|
+
createFoirLiveManager,
|
|
825
|
+
createFoirLiveStore,
|
|
826
|
+
createFoirReconciler,
|
|
827
|
+
createFoirTagIndex,
|
|
828
|
+
foirDecimalKey,
|
|
829
|
+
foirDriftIsActionable,
|
|
830
|
+
foirHeldRecordIds,
|
|
831
|
+
foirModelQueryKey,
|
|
832
|
+
foirModelTag,
|
|
833
|
+
foirQueryKey,
|
|
834
|
+
foirQueryKeys,
|
|
835
|
+
foirRecordTag,
|
|
836
|
+
foirRequiresResync,
|
|
837
|
+
foirResumeCursor,
|
|
838
|
+
foirStableKey,
|
|
839
|
+
foirTagsForGoneRecords,
|
|
840
|
+
foirTagsForQueryInvalidation,
|
|
841
|
+
foirTagsForRecordChange,
|
|
842
|
+
foirWriteTokenHeaders,
|
|
843
|
+
readFoirLiveHeaders,
|
|
844
|
+
readFoirWriteToken
|
|
845
|
+
};
|