@supalive/core 1.20.1 → 1.21.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.
Files changed (68) hide show
  1. package/dist/ephemeral-BVeIVuYw.js +276 -0
  2. package/dist/ephemeral-BVeIVuYw.js.map +1 -0
  3. package/dist/ephemeral-CkidSG9y.js +295 -0
  4. package/dist/ephemeral-CkidSG9y.js.map +1 -0
  5. package/dist/index-CzW0TkpK.d.ts +2729 -0
  6. package/dist/index-CzW0TkpK.d.ts.map +1 -0
  7. package/dist/index-DB7_pv9P.d.ts +2746 -0
  8. package/dist/index-DB7_pv9P.d.ts.map +1 -0
  9. package/dist/logger-BJNCJkFh.js +463 -0
  10. package/dist/logger-BJNCJkFh.js.map +1 -0
  11. package/dist/logger-By9Pw-X2.js +454 -0
  12. package/dist/logger-By9Pw-X2.js.map +1 -0
  13. package/dist/mysql-Bvc_iWUy.d.ts +114 -0
  14. package/dist/mysql-Bvc_iWUy.d.ts.map +1 -0
  15. package/dist/mysql-CcIuIAt0.js +622 -0
  16. package/dist/mysql-CcIuIAt0.js.map +1 -0
  17. package/dist/mysql-DTK2f5oU.d.ts +114 -0
  18. package/dist/mysql-DTK2f5oU.d.ts.map +1 -0
  19. package/dist/one-shot-query-BNYLU19W.js +573 -0
  20. package/dist/one-shot-query-BNYLU19W.js.map +1 -0
  21. package/dist/one-shot-query-BSK7adwG.js +573 -0
  22. package/dist/one-shot-query-BSK7adwG.js.map +1 -0
  23. package/dist/one-shot-query-DAAHGTcI.js +573 -0
  24. package/dist/one-shot-query-DAAHGTcI.js.map +1 -0
  25. package/dist/overlap-checker-DseE1Lvh.js +300 -0
  26. package/dist/overlap-checker-DseE1Lvh.js.map +1 -0
  27. package/dist/postgres-Bp7SMb9D.d.ts +118 -0
  28. package/dist/postgres-Bp7SMb9D.d.ts.map +1 -0
  29. package/dist/postgres-C5CbPBcp.d.ts +118 -0
  30. package/dist/postgres-C5CbPBcp.d.ts.map +1 -0
  31. package/dist/postgres-CLhoKn_R.js +867 -0
  32. package/dist/postgres-CLhoKn_R.js.map +1 -0
  33. package/dist/realtime_db-CTGOrFuV.js +463 -0
  34. package/dist/realtime_db-CTGOrFuV.js.map +1 -0
  35. package/dist/src/client/index.d.ts +2 -2
  36. package/dist/src/client/index.js +6 -5
  37. package/dist/src/client/index.js.map +1 -1
  38. package/dist/src/exports/mysql.d.ts +1 -1
  39. package/dist/src/exports/mysql.js +1 -1
  40. package/dist/src/exports/postgres.d.ts +1 -1
  41. package/dist/src/exports/postgres.js +1 -1
  42. package/dist/src/exports/procedure.d.ts +2 -2
  43. package/dist/src/exports/procedure.d.ts.map +1 -1
  44. package/dist/src/exports/procedure.js +2 -1
  45. package/dist/src/exports/procedure.js.map +1 -1
  46. package/dist/src/exports/schema-sql.d.ts +1 -1
  47. package/dist/src/exports/server.d.ts +56 -5
  48. package/dist/src/exports/server.d.ts.map +1 -1
  49. package/dist/src/exports/server.js +168 -10
  50. package/dist/src/exports/server.js.map +1 -1
  51. package/dist/src/exports/sub-manager-worker-entry.js +1 -1
  52. package/dist/src/exports/types.d.ts +2 -2
  53. package/dist/src/exports/types.js +2 -2
  54. package/dist/sub-worker-dispatch-C20KTMfg.js +925 -0
  55. package/dist/sub-worker-dispatch-C20KTMfg.js.map +1 -0
  56. package/dist/sub-worker-dispatch-CQCuwZPm.js +925 -0
  57. package/dist/sub-worker-dispatch-CQCuwZPm.js.map +1 -0
  58. package/dist/sub-worker-dispatch-Dwrp3c40.js +925 -0
  59. package/dist/sub-worker-dispatch-Dwrp3c40.js.map +1 -0
  60. package/dist/types_client_rpc-CqUVHTwT.js +163 -0
  61. package/dist/types_client_rpc-CqUVHTwT.js.map +1 -0
  62. package/dist/types_db-DPMeNETt.js +173 -0
  63. package/dist/types_db-DPMeNETt.js.map +1 -0
  64. package/dist/types_server-hiwqDm4G.d.ts +737 -0
  65. package/dist/types_server-hiwqDm4G.d.ts.map +1 -0
  66. package/dist/types_server-xHmZ6baD.d.ts +737 -0
  67. package/dist/types_server-xHmZ6baD.d.ts.map +1 -0
  68. package/package.json +1 -1
@@ -0,0 +1,276 @@
1
+ import { r as supaliveStringify } from "./helper-zdJT5FUc.js";
2
+ //#region src/db/ephemeral.ts
3
+ /**
4
+ * Default lifetime of an ephemeral entry: 7 days. Long enough that a value set
5
+ * once (a device's last known position, a presence flag) survives normal
6
+ * operation, short enough that abandoned keys eventually drain out of Redis on
7
+ * their own. Override per call via {@link EphemeralSetOptions.ttlSeconds}.
8
+ */
9
+ const EPHEMERAL_DEFAULT_TTL_SECONDS = 10080 * 60;
10
+ /**
11
+ * Shared stand-in for "this computation read no ephemeral state" — the
12
+ * overwhelmingly common case. Frozen and reused so neither a `TxContext` nor a
13
+ * `LiveResult` allocates an array for a query that never touches the store.
14
+ */
15
+ const EMPTY_EPHEMERAL_KEYS = Object.freeze([]);
16
+ /** Redis key holding the value for an ephemeral entry. */
17
+ function valueKeyOf(key) {
18
+ return `sl:eph:${key}`;
19
+ }
20
+ /**
21
+ * Single global monotonic counter bumped by every ephemeral write. Its value is
22
+ * the `version` handed to the invalidation path, where it plays exactly the role
23
+ * a mutation's `commitTs` plays: the discriminator in the cross-instance
24
+ * recompute lock (`lock:recompute:<subId>:<version>`), so every instance racing
25
+ * to recompute the same write agrees on which write it is racing for.
26
+ *
27
+ * It deliberately is NOT per-key: a recompute always re-reads the current value
28
+ * from Redis, so the version only has to be unique per write *event*, never a
29
+ * meaningful per-key revision.
30
+ */
31
+ const SEQ_KEY = "sl:eph:seq";
32
+ /**
33
+ * Apply a batch of ephemeral writes and return the new global sequence.
34
+ *
35
+ * KEYS[1..n] — value keys; KEYS[n+1] — the sequence key.
36
+ * ARGV[2i-1] — serialized value for KEYS[i]; ARGV[2i] — its TTL in seconds,
37
+ * where `0` means "delete instead of set".
38
+ *
39
+ * A single script keeps the whole batch atomic and costs one round trip no
40
+ * matter how many keys a mutation buffered, and the trailing INCR allocates the
41
+ * version *after* every value is visible — so a recompute triggered by this
42
+ * version can never read a pre-write value.
43
+ */
44
+ const APPLY_EPHEMERAL_WRITES = `
45
+ local n = #KEYS - 1
46
+ for i = 1, n do
47
+ local ttl = tonumber(ARGV[i * 2])
48
+ if ttl <= 0 then
49
+ redis.call('DEL', KEYS[i])
50
+ else
51
+ redis.call('SET', KEYS[i], ARGV[i * 2 - 1], 'EX', ttl)
52
+ end
53
+ end
54
+ return redis.call('INCR', KEYS[n + 1])`;
55
+ /** Thrown by {@link EphemeralReader.get} when the key holds no value. */
56
+ var EphemeralKeyNotFoundError = class extends Error {
57
+ key;
58
+ constructor(key) {
59
+ super(`Ephemeral key not found: ${key}`);
60
+ this.key = key;
61
+ this.name = "EphemeralKeyNotFoundError";
62
+ }
63
+ };
64
+ /**
65
+ * Thrown when a stored value does not match the schema the reader asked for —
66
+ * corrupt bytes, or (far more likely) a value written by an older deploy under
67
+ * a shape that has since changed.
68
+ *
69
+ * This is a hard error rather than a silent `null` because the two are not the
70
+ * same thing: `null` means "nothing is here", and quietly conflating a schema
71
+ * mismatch with absence turns a deploy bug into a blank screen. A caller that
72
+ * genuinely wants tolerance can express it in the schema itself — `.catch()`,
73
+ * `.optional()`, or a union — which is strictly more expressive than anything
74
+ * this layer could decide on its behalf.
75
+ */
76
+ var EphemeralDecodeError = class extends Error {
77
+ key;
78
+ constructor(key, options) {
79
+ super(`Ephemeral value at "${key}" did not match the expected schema`, options);
80
+ this.key = key;
81
+ this.name = "EphemeralDecodeError";
82
+ }
83
+ };
84
+ /**
85
+ * Redis-backed store for state that drives live queries but is never persisted
86
+ * to the database — a device's latest GPS fix, a "typing…" flag, a transient
87
+ * job progress percentage.
88
+ *
89
+ * Unlike a table row, an ephemeral key has no commit log and no snapshot ts, so
90
+ * it cannot participate in the OCC read set or the commit-log freshness replay.
91
+ * Instead a query records the plain key strings it read
92
+ * (`LiveResult.ephemeralKeys`), the sub-manager indexes subscriptions by those
93
+ * keys, and a write looks subscribers up by exact key match — cheaper than the
94
+ * write-set scan, because there is no predicate to evaluate.
95
+ */
96
+ var EphemeralStore = class {
97
+ cache;
98
+ defaultTtlSeconds;
99
+ /**
100
+ * Fired after a batch of writes lands in Redis, with the affected keys and the
101
+ * newly allocated global version. The server wires this to the sub-manager's
102
+ * ephemeral invalidation, mirroring how `SupaliveDb.setOnAfterCommit` drives
103
+ * write-set invalidation. Left unset outside a server (tests, tooling), where
104
+ * writes are then purely local.
105
+ */
106
+ onAfterWrite;
107
+ constructor(cache, opts) {
108
+ this.cache = cache;
109
+ this.defaultTtlSeconds = opts?.defaultTtlSeconds ?? 604800;
110
+ }
111
+ /** The TTL a `set` without an explicit `ttlSeconds` uses. */
112
+ get defaultTtl() {
113
+ return this.defaultTtlSeconds;
114
+ }
115
+ async find(key, schema) {
116
+ return decode(key, await this.cache.ephemeralGet(valueKeyOf(key)), schema);
117
+ }
118
+ async get(key, schema) {
119
+ const value = await this.find(key, schema);
120
+ if (value === null) throw new EphemeralKeyNotFoundError(key);
121
+ return value;
122
+ }
123
+ async getOr(key, schema, fallback) {
124
+ const value = await this.find(key, schema);
125
+ return value === null ? fallback : value;
126
+ }
127
+ async findMany(keys, schema) {
128
+ if (keys.length === 0) return [];
129
+ return (await this.cache.ephemeralGetMany(keys.map(valueKeyOf))).map((r, i) => decode(keys[i], r, schema));
130
+ }
131
+ async has(key) {
132
+ return await this.cache.ephemeralGet(valueKeyOf(key)) !== null;
133
+ }
134
+ async set(key, value, opts) {
135
+ await this.apply([this.prepareSet(key, value, opts)]);
136
+ }
137
+ async delete(key) {
138
+ await this.apply([this.prepareDelete(key)]);
139
+ }
140
+ /**
141
+ * Serialize a `set` into a buffered write WITHOUT touching Redis. Used by
142
+ * `DbWriter`, which defers ephemeral writes until its transaction commits.
143
+ */
144
+ prepareSet(key, value, opts) {
145
+ const ttlSeconds = opts?.ttlSeconds ?? this.defaultTtlSeconds;
146
+ if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) throw new Error(`Ephemeral ttlSeconds must be a positive number (got ${ttlSeconds})`);
147
+ return {
148
+ key,
149
+ value: supaliveStringify(value),
150
+ ttlSeconds: Math.ceil(ttlSeconds)
151
+ };
152
+ }
153
+ /** The `delete` counterpart of {@link prepareSet}. */
154
+ prepareDelete(key) {
155
+ return {
156
+ key,
157
+ value: "",
158
+ ttlSeconds: 0
159
+ };
160
+ }
161
+ /**
162
+ * Apply buffered writes atomically, then notify the invalidation hook. The
163
+ * hook runs only after Redis holds the new values, so any recompute it
164
+ * triggers is guaranteed to read them.
165
+ */
166
+ async apply(writes) {
167
+ if (writes.length === 0) return;
168
+ const version = await this.cache.evalScript(APPLY_EPHEMERAL_WRITES, [...writes.map((w) => valueKeyOf(w.key)), SEQ_KEY], writes.flatMap((w) => [w.value, String(w.ttlSeconds)]));
169
+ this.onAfterWrite?.(writes.map((w) => w.key), String(version));
170
+ }
171
+ };
172
+ function decode(key, raw, schema) {
173
+ if (raw === null || raw === void 0) return null;
174
+ let parsed;
175
+ try {
176
+ parsed = JSON.parse(raw);
177
+ } catch (cause) {
178
+ throw new EphemeralDecodeError(key, { cause });
179
+ }
180
+ const result = schema.safeParse(parsed);
181
+ if (!result.success) throw new EphemeralDecodeError(key, { cause: result.error });
182
+ return result.data;
183
+ }
184
+ /**
185
+ * An {@link EphemeralStore} backed by a plain Map instead of Redis — for unit
186
+ * tests and tooling that construct a `SupaliveDb` without a cache layer.
187
+ *
188
+ * It is process-local, so it is NOT a substitute for the real store in any
189
+ * multi-instance deployment: two servers would each see their own values, and
190
+ * cross-instance invalidation would push stale data. TTLs are honoured on read
191
+ * (there is no background expiry), which is enough for the semantics tests care
192
+ * about.
193
+ */
194
+ function createMemoryEphemeralStore(opts) {
195
+ const values = /* @__PURE__ */ new Map();
196
+ let seq = 0;
197
+ const read = (key) => {
198
+ const entry = values.get(key);
199
+ if (!entry) return null;
200
+ if (entry.expiresAt <= Date.now()) {
201
+ values.delete(key);
202
+ return null;
203
+ }
204
+ return entry.value;
205
+ };
206
+ return new EphemeralStore({
207
+ ephemeralGet: async (key) => read(key),
208
+ ephemeralGetMany: async (keys) => keys.map(read),
209
+ evalScript: async (_src, keys, args) => {
210
+ for (let i = 0; i < keys.length - 1; i++) {
211
+ const ttl = Number(args[i * 2 + 1]);
212
+ if (ttl <= 0) values.delete(keys[i]);
213
+ else values.set(keys[i], {
214
+ value: String(args[i * 2]),
215
+ expiresAt: Date.now() + ttl * 1e3
216
+ });
217
+ }
218
+ return ++seq;
219
+ }
220
+ }, opts);
221
+ }
222
+ /**
223
+ * Wrap `store` so every read also records a live-query dependency on the key it
224
+ * touched. The dependency is on the KEY, not the value: reading a key that is
225
+ * currently absent still subscribes you to it being set later.
226
+ */
227
+ function createTrackedEphemeralReader(ctx, store) {
228
+ return {
229
+ find: (key, schema) => {
230
+ ctx.internalTrackEphemeralRead(key);
231
+ return store.find(key, schema);
232
+ },
233
+ get: (key, schema) => {
234
+ ctx.internalTrackEphemeralRead(key);
235
+ return store.get(key, schema);
236
+ },
237
+ getOr: (key, schema, fallback) => {
238
+ ctx.internalTrackEphemeralRead(key);
239
+ return store.getOr(key, schema, fallback);
240
+ },
241
+ findMany: (keys, schema) => {
242
+ for (const key of keys) ctx.internalTrackEphemeralRead(key);
243
+ return store.findMany(keys, schema);
244
+ },
245
+ has: (key) => {
246
+ ctx.internalTrackEphemeralRead(key);
247
+ return store.has(key);
248
+ }
249
+ };
250
+ }
251
+ /**
252
+ * Wrap `store` for use inside a mutation: reads track as above, but writes are
253
+ * BUFFERED on the context and applied only once the transaction commits.
254
+ *
255
+ * Buffering is what makes ephemeral writes safe in a transactional handler. A
256
+ * mutation body can run several times — the OCC retry policy re-invokes it with
257
+ * a fresh `DbWriter` on every conflict — so writing to Redis eagerly would
258
+ * publish state from attempts that never committed, and push it to subscribers.
259
+ * Only the committing attempt's buffer is flushed; a losing attempt's buffer is
260
+ * discarded with its context.
261
+ */
262
+ function createBufferedEphemeralWriter(ctx, store) {
263
+ return {
264
+ ...createTrackedEphemeralReader(ctx, store),
265
+ set: async (key, value, opts) => {
266
+ ctx.internalBufferEphemeralWrite(store.prepareSet(key, value, opts));
267
+ },
268
+ delete: async (key) => {
269
+ ctx.internalBufferEphemeralWrite(store.prepareDelete(key));
270
+ }
271
+ };
272
+ }
273
+ //#endregion
274
+ export { EphemeralStore as a, createTrackedEphemeralReader as c, EphemeralKeyNotFoundError as i, EPHEMERAL_DEFAULT_TTL_SECONDS as n, createBufferedEphemeralWriter as o, EphemeralDecodeError as r, createMemoryEphemeralStore as s, EMPTY_EPHEMERAL_KEYS as t };
275
+
276
+ //# sourceMappingURL=ephemeral-BVeIVuYw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ephemeral-BVeIVuYw.js","names":[],"sources":["../src/db/ephemeral.ts"],"sourcesContent":["import type { ZodType } from \"zod\";\nimport type { CacheLayer } from \"./cache\";\nimport { supaliveStringify } from \"../helper\";\n\n/**\n * Default lifetime of an ephemeral entry: 7 days. Long enough that a value set\n * once (a device's last known position, a presence flag) survives normal\n * operation, short enough that abandoned keys eventually drain out of Redis on\n * their own. Override per call via {@link EphemeralSetOptions.ttlSeconds}.\n */\nexport const EPHEMERAL_DEFAULT_TTL_SECONDS = 7 * 24 * 60 * 60;\n\n/**\n * Shared stand-in for \"this computation read no ephemeral state\" — the\n * overwhelmingly common case. Frozen and reused so neither a `TxContext` nor a\n * `LiveResult` allocates an array for a query that never touches the store.\n */\nexport const EMPTY_EPHEMERAL_KEYS: string[] = Object.freeze([] as string[]) as string[];\n\n/** Redis key holding the value for an ephemeral entry. */\nfunction valueKeyOf(key: string): string {\n return `sl:eph:${key}`;\n}\n\n/**\n * Single global monotonic counter bumped by every ephemeral write. Its value is\n * the `version` handed to the invalidation path, where it plays exactly the role\n * a mutation's `commitTs` plays: the discriminator in the cross-instance\n * recompute lock (`lock:recompute:<subId>:<version>`), so every instance racing\n * to recompute the same write agrees on which write it is racing for.\n *\n * It deliberately is NOT per-key: a recompute always re-reads the current value\n * from Redis, so the version only has to be unique per write *event*, never a\n * meaningful per-key revision.\n */\nconst SEQ_KEY = \"sl:eph:seq\";\n\n/**\n * Apply a batch of ephemeral writes and return the new global sequence.\n *\n * KEYS[1..n] — value keys; KEYS[n+1] — the sequence key.\n * ARGV[2i-1] — serialized value for KEYS[i]; ARGV[2i] — its TTL in seconds,\n * where `0` means \"delete instead of set\".\n *\n * A single script keeps the whole batch atomic and costs one round trip no\n * matter how many keys a mutation buffered, and the trailing INCR allocates the\n * version *after* every value is visible — so a recompute triggered by this\n * version can never read a pre-write value.\n */\nconst APPLY_EPHEMERAL_WRITES = `\nlocal n = #KEYS - 1\nfor i = 1, n do\n local ttl = tonumber(ARGV[i * 2])\n if ttl <= 0 then\n redis.call('DEL', KEYS[i])\n else\n redis.call('SET', KEYS[i], ARGV[i * 2 - 1], 'EX', ttl)\n end\nend\nreturn redis.call('INCR', KEYS[n + 1])`;\n\n/** Thrown by {@link EphemeralReader.get} when the key holds no value. */\nexport class EphemeralKeyNotFoundError extends Error {\n constructor(readonly key: string) {\n super(`Ephemeral key not found: ${key}`);\n this.name = \"EphemeralKeyNotFoundError\";\n }\n}\n\n/**\n * Thrown when a stored value does not match the schema the reader asked for —\n * corrupt bytes, or (far more likely) a value written by an older deploy under\n * a shape that has since changed.\n *\n * This is a hard error rather than a silent `null` because the two are not the\n * same thing: `null` means \"nothing is here\", and quietly conflating a schema\n * mismatch with absence turns a deploy bug into a blank screen. A caller that\n * genuinely wants tolerance can express it in the schema itself — `.catch()`,\n * `.optional()`, or a union — which is strictly more expressive than anything\n * this layer could decide on its behalf.\n */\nexport class EphemeralDecodeError extends Error {\n constructor(readonly key: string, options?: { cause?: unknown }) {\n super(`Ephemeral value at \"${key}\" did not match the expected schema`, options);\n this.name = \"EphemeralDecodeError\";\n }\n}\n\nexport interface EphemeralSetOptions {\n /**\n * Lifetime in seconds. Defaults to {@link EPHEMERAL_DEFAULT_TTL_SECONDS}\n * (7 days), or to the server-configured default.\n *\n * Expiry is silent: when the TTL elapses the value simply disappears and the\n * next recompute of a subscribed query observes `null`. It does NOT by itself\n * push an update — call {@link EphemeralWriter.delete} when subscribers need\n * to see the value go away at a specific moment.\n */\n ttlSeconds?: number;\n}\n\n/**\n * Read side of the ephemeral store.\n *\n * Reached from a query/mutation handler as `ctx.db.ephemeral`, where every read\n * also records a live-query dependency on the key, and from an action/job as\n * `ctx.db.ephemeral` on the {@link SupaliveDb} itself (untracked — an action has\n * no read set).\n *\n * Every read takes the Zod schema its value should satisfy. Ephemeral state is\n * schemaless in Redis — no table, no column types, and its writer may be a\n * different deploy of your own code — so a `as T` cast here would be a type\n * that is checked nowhere and true only by convention. Passing the schema makes\n * the boundary an actual parse.\n *\n * The naming mirrors the SQL query builder: {@link find} returns `null` for a\n * missing key exactly like `query(Schema).find(id)` does, {@link get} is the\n * \"I know it's there\" variant that throws, and {@link getOr} supplies a default.\n */\nexport interface EphemeralReader {\n /** Value for `key`, or `null` when it is absent or expired. */\n find<T>(key: string, schema: ZodType<T>): Promise<T | null>;\n /** Value for `key`. Throws {@link EphemeralKeyNotFoundError} when absent. */\n get<T>(key: string, schema: ZodType<T>): Promise<T>;\n /** Value for `key`, or `fallback` when it is absent or expired. */\n getOr<T>(key: string, schema: ZodType<T>, fallback: T): Promise<T>;\n /** Values for `keys`, positionally aligned; `null` for each absent key. */\n findMany<T>(keys: string[], schema: ZodType<T>): Promise<(T | null)[]>;\n /** Whether `key` currently holds a value. Does not decode it, so it needs no\n * schema — and a value that fails to parse still counts as present. */\n has(key: string): Promise<boolean>;\n}\n\n/** Read side plus writes. See {@link EphemeralReader} for the read semantics. */\nexport interface EphemeralWriter extends EphemeralReader {\n /** Store `value` under `key`, replacing any existing entry. */\n set<T>(key: string, value: T, opts?: EphemeralSetOptions): Promise<void>;\n /** Remove `key`. Invalidates subscribers just like a `set` does. */\n delete(key: string): Promise<void>;\n}\n\n/** A single pending write in a `DbWriter`'s buffer. `ttlSeconds: 0` = delete. */\nexport interface PendingEphemeralWrite {\n key: string;\n /** Pre-serialized payload; empty string for a delete. */\n value: string;\n ttlSeconds: number;\n}\n\n/**\n * Redis-backed store for state that drives live queries but is never persisted\n * to the database — a device's latest GPS fix, a \"typing…\" flag, a transient\n * job progress percentage.\n *\n * Unlike a table row, an ephemeral key has no commit log and no snapshot ts, so\n * it cannot participate in the OCC read set or the commit-log freshness replay.\n * Instead a query records the plain key strings it read\n * (`LiveResult.ephemeralKeys`), the sub-manager indexes subscriptions by those\n * keys, and a write looks subscribers up by exact key match — cheaper than the\n * write-set scan, because there is no predicate to evaluate.\n */\nexport class EphemeralStore implements EphemeralWriter {\n private readonly cache: CacheLayer;\n private readonly defaultTtlSeconds: number;\n\n /**\n * Fired after a batch of writes lands in Redis, with the affected keys and the\n * newly allocated global version. The server wires this to the sub-manager's\n * ephemeral invalidation, mirroring how `SupaliveDb.setOnAfterCommit` drives\n * write-set invalidation. Left unset outside a server (tests, tooling), where\n * writes are then purely local.\n */\n onAfterWrite?: (keys: string[], version: string) => void;\n\n constructor(cache: CacheLayer, opts?: { defaultTtlSeconds?: number }) {\n this.cache = cache;\n this.defaultTtlSeconds = opts?.defaultTtlSeconds ?? EPHEMERAL_DEFAULT_TTL_SECONDS;\n }\n\n /** The TTL a `set` without an explicit `ttlSeconds` uses. */\n get defaultTtl(): number {\n return this.defaultTtlSeconds;\n }\n\n async find<T>(key: string, schema: ZodType<T>): Promise<T | null> {\n const raw = await this.cache.ephemeralGet(valueKeyOf(key));\n return decode(key, raw, schema);\n }\n\n async get<T>(key: string, schema: ZodType<T>): Promise<T> {\n const value = await this.find(key, schema);\n if (value === null) throw new EphemeralKeyNotFoundError(key);\n return value;\n }\n\n async getOr<T>(key: string, schema: ZodType<T>, fallback: T): Promise<T> {\n const value = await this.find(key, schema);\n return value === null ? fallback : value;\n }\n\n async findMany<T>(keys: string[], schema: ZodType<T>): Promise<(T | null)[]> {\n if (keys.length === 0) return [];\n const raw = await this.cache.ephemeralGetMany(keys.map(valueKeyOf));\n return raw.map((r, i) => decode(keys[i]!, r, schema));\n }\n\n async has(key: string): Promise<boolean> {\n return (await this.cache.ephemeralGet(valueKeyOf(key))) !== null;\n }\n\n async set<T>(key: string, value: T, opts?: EphemeralSetOptions): Promise<void> {\n await this.apply([this.prepareSet(key, value, opts)]);\n }\n\n async delete(key: string): Promise<void> {\n await this.apply([this.prepareDelete(key)]);\n }\n\n /**\n * Serialize a `set` into a buffered write WITHOUT touching Redis. Used by\n * `DbWriter`, which defers ephemeral writes until its transaction commits.\n */\n prepareSet<T>(key: string, value: T, opts?: EphemeralSetOptions): PendingEphemeralWrite {\n const ttlSeconds = opts?.ttlSeconds ?? this.defaultTtlSeconds;\n if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {\n throw new Error(`Ephemeral ttlSeconds must be a positive number (got ${ttlSeconds})`);\n }\n return { key, value: supaliveStringify(value), ttlSeconds: Math.ceil(ttlSeconds) };\n }\n\n /** The `delete` counterpart of {@link prepareSet}. */\n prepareDelete(key: string): PendingEphemeralWrite {\n return { key, value: \"\", ttlSeconds: 0 };\n }\n\n /**\n * Apply buffered writes atomically, then notify the invalidation hook. The\n * hook runs only after Redis holds the new values, so any recompute it\n * triggers is guaranteed to read them.\n */\n async apply(writes: PendingEphemeralWrite[]): Promise<void> {\n if (writes.length === 0) return;\n\n const version = await this.cache.evalScript(\n APPLY_EPHEMERAL_WRITES,\n [...writes.map((w) => valueKeyOf(w.key)), SEQ_KEY],\n writes.flatMap((w) => [w.value, String(w.ttlSeconds)]),\n );\n\n this.onAfterWrite?.(writes.map((w) => w.key), String(version));\n }\n}\n\nfunction decode<T>(key: string, raw: string | null, schema: ZodType<T>): T | null {\n if (raw === null || raw === undefined) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (cause) {\n throw new EphemeralDecodeError(key, { cause });\n }\n\n const result = schema.safeParse(parsed);\n if (!result.success) throw new EphemeralDecodeError(key, { cause: result.error });\n return result.data;\n}\n\n/**\n * An {@link EphemeralStore} backed by a plain Map instead of Redis — for unit\n * tests and tooling that construct a `SupaliveDb` without a cache layer.\n *\n * It is process-local, so it is NOT a substitute for the real store in any\n * multi-instance deployment: two servers would each see their own values, and\n * cross-instance invalidation would push stale data. TTLs are honoured on read\n * (there is no background expiry), which is enough for the semantics tests care\n * about.\n */\nexport function createMemoryEphemeralStore(opts?: { defaultTtlSeconds?: number }): EphemeralStore {\n const values = new Map<string, { value: string; expiresAt: number }>();\n let seq = 0;\n\n const read = (key: string): string | null => {\n const entry = values.get(key);\n if (!entry) return null;\n if (entry.expiresAt <= Date.now()) {\n values.delete(key);\n return null;\n }\n return entry.value;\n };\n\n const backing = {\n ephemeralGet: async (key: string) => read(key),\n ephemeralGetMany: async (keys: string[]) => keys.map(read),\n // Mirrors APPLY_EPHEMERAL_WRITES: the trailing key is the sequence, and each\n // value key carries a (value, ttl) ARGV pair where ttl <= 0 means delete.\n evalScript: async (_src: string, keys: string[], args: (string | number)[]) => {\n for (let i = 0; i < keys.length - 1; i++) {\n const ttl = Number(args[i * 2 + 1]);\n if (ttl <= 0) values.delete(keys[i]!);\n else values.set(keys[i]!, { value: String(args[i * 2]), expiresAt: Date.now() + ttl * 1000 });\n }\n return ++seq;\n },\n } as unknown as CacheLayer;\n\n return new EphemeralStore(backing, opts);\n}\n\n// ─── Handler-facing wrappers ─────────────────────────────────────────────────\n//\n// Built as free functions over a context rather than methods on it: the tracking\n// and buffering they add is the ONLY difference from the bare store, and keeping\n// them out of `DbReader`/`DbWriter` leaves those classes about SQL. Each takes\n// the context it reports to as its first argument.\n\n/** The slice of a `DbReader` these wrappers drive. Structural, so `context.ts`\n * doesn't have to be imported here (it imports this module). */\nexport interface EphemeralReadTracker {\n internalTrackEphemeralRead(key: string): void;\n}\n\n/** The slice of a `DbWriter` the write wrapper drives. */\nexport interface EphemeralWriteBuffer extends EphemeralReadTracker {\n internalBufferEphemeralWrite(write: PendingEphemeralWrite): void;\n}\n\n/**\n * Wrap `store` so every read also records a live-query dependency on the key it\n * touched. The dependency is on the KEY, not the value: reading a key that is\n * currently absent still subscribes you to it being set later.\n */\nexport function createTrackedEphemeralReader(\n ctx: EphemeralReadTracker,\n store: EphemeralStore,\n): EphemeralReader {\n return {\n find: (key, schema) => {\n ctx.internalTrackEphemeralRead(key);\n return store.find(key, schema);\n },\n get: (key, schema) => {\n ctx.internalTrackEphemeralRead(key);\n return store.get(key, schema);\n },\n getOr: (key, schema, fallback) => {\n ctx.internalTrackEphemeralRead(key);\n return store.getOr(key, schema, fallback);\n },\n findMany: (keys, schema) => {\n for (const key of keys) ctx.internalTrackEphemeralRead(key);\n return store.findMany(keys, schema);\n },\n has: (key) => {\n ctx.internalTrackEphemeralRead(key);\n return store.has(key);\n },\n };\n}\n\n/**\n * Wrap `store` for use inside a mutation: reads track as above, but writes are\n * BUFFERED on the context and applied only once the transaction commits.\n *\n * Buffering is what makes ephemeral writes safe in a transactional handler. A\n * mutation body can run several times — the OCC retry policy re-invokes it with\n * a fresh `DbWriter` on every conflict — so writing to Redis eagerly would\n * publish state from attempts that never committed, and push it to subscribers.\n * Only the committing attempt's buffer is flushed; a losing attempt's buffer is\n * discarded with its context.\n */\nexport function createBufferedEphemeralWriter(\n ctx: EphemeralWriteBuffer,\n store: EphemeralStore,\n): EphemeralWriter {\n return {\n ...createTrackedEphemeralReader(ctx, store),\n set: async (key, value, opts) => {\n ctx.internalBufferEphemeralWrite(store.prepareSet(key, value, opts));\n },\n delete: async (key) => {\n ctx.internalBufferEphemeralWrite(store.prepareDelete(key));\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,295 @@
1
+ import { r as supaliveStringify } from "./helper-zdJT5FUc.js";
2
+ //#region src/db/ephemeral.ts
3
+ /**
4
+ * Default lifetime of an ephemeral entry: 7 days. Long enough that a value set
5
+ * once (a device's last known position, a presence flag) survives normal
6
+ * operation, short enough that abandoned keys eventually drain out of Redis on
7
+ * their own. Override per call via {@link EphemeralSetOptions.ttlSeconds}.
8
+ */
9
+ const EPHEMERAL_DEFAULT_TTL_SECONDS = 10080 * 60;
10
+ /**
11
+ * Shared stand-in for "this computation read no ephemeral state" — the
12
+ * overwhelmingly common case. Frozen and reused so neither a `TxContext` nor a
13
+ * `LiveResult` allocates an array for a query that never touches the store.
14
+ */
15
+ const EMPTY_EPHEMERAL_KEYS = Object.freeze([]);
16
+ /** Redis key holding the value for an ephemeral entry. */
17
+ function valueKeyOf(key) {
18
+ return `sl:eph:${key}`;
19
+ }
20
+ /**
21
+ * Single global monotonic counter bumped by every ephemeral write. Its value is
22
+ * the `version` handed to the invalidation path, where it plays exactly the role
23
+ * a mutation's `commitTs` plays: the discriminator in the cross-instance
24
+ * recompute lock (`lock:recompute:<subId>:<version>`), so every instance racing
25
+ * to recompute the same write agrees on which write it is racing for.
26
+ *
27
+ * It deliberately is NOT per-key: a recompute always re-reads the current value
28
+ * from Redis, so the version only has to be unique per write *event*, never a
29
+ * meaningful per-key revision.
30
+ */
31
+ const SEQ_KEY = "sl:eph:seq";
32
+ /**
33
+ * Apply a batch of ephemeral writes and return the new global sequence.
34
+ *
35
+ * KEYS[1..n] — value keys; KEYS[n+1] — the sequence key.
36
+ * ARGV[2i-1] — serialized value for KEYS[i]; ARGV[2i] — its TTL in seconds,
37
+ * where `0` means "delete instead of set".
38
+ *
39
+ * A single script keeps the whole batch atomic and costs one round trip no
40
+ * matter how many keys a mutation buffered, and the trailing INCR allocates the
41
+ * version *after* every value is visible — so a recompute triggered by this
42
+ * version can never read a pre-write value.
43
+ */
44
+ const APPLY_EPHEMERAL_WRITES = `
45
+ local n = #KEYS - 1
46
+ for i = 1, n do
47
+ local ttl = tonumber(ARGV[i * 2])
48
+ if ttl <= 0 then
49
+ redis.call('DEL', KEYS[i])
50
+ else
51
+ redis.call('SET', KEYS[i], ARGV[i * 2 - 1], 'EX', ttl)
52
+ end
53
+ end
54
+ return redis.call('INCR', KEYS[n + 1])`;
55
+ /** Thrown by {@link EphemeralReader.get} when the key holds no value. */
56
+ var EphemeralKeyNotFoundError = class extends Error {
57
+ key;
58
+ constructor(key) {
59
+ super(`Ephemeral key not found: ${key}`);
60
+ this.key = key;
61
+ this.name = "EphemeralKeyNotFoundError";
62
+ }
63
+ };
64
+ /**
65
+ * Thrown when a stored value does not match the schema the reader asked for —
66
+ * corrupt bytes, or (far more likely) a value written by an older deploy under
67
+ * a shape that has since changed.
68
+ *
69
+ * This is a hard error rather than a silent `null` because the two are not the
70
+ * same thing: `null` means "nothing is here", and quietly conflating a schema
71
+ * mismatch with absence turns a deploy bug into a blank screen. A caller that
72
+ * genuinely wants tolerance can express it in the schema itself — `.catch()`,
73
+ * `.optional()`, or a union — which is strictly more expressive than anything
74
+ * this layer could decide on its behalf.
75
+ */
76
+ var EphemeralDecodeError = class extends Error {
77
+ key;
78
+ constructor(key, options) {
79
+ super(`Ephemeral value at "${key}" did not match the expected schema`, options);
80
+ this.key = key;
81
+ this.name = "EphemeralDecodeError";
82
+ }
83
+ };
84
+ /**
85
+ * Thrown when a mutation's buffered ephemeral writes could not be applied after
86
+ * its transaction committed.
87
+ *
88
+ * The distinction this type exists to make: **the mutation succeeded**. Its rows
89
+ * are written and durable, and their subscription invalidation has already gone
90
+ * out. Only the ephemeral half — which lives in Redis, outside the transaction —
91
+ * failed to land. A caller must NOT respond by retrying the mutation; that would
92
+ * apply the row writes a second time. Retry the ephemeral write alone, or let
93
+ * the next write to those keys carry the state forward.
94
+ */
95
+ var EphemeralFlushError = class extends Error {
96
+ keys;
97
+ constructor(keys, options) {
98
+ super(`Mutation committed, but its ephemeral writes failed to apply (keys: ${keys.join(", ")}). The database changes are durable — do not retry the mutation.`, options);
99
+ this.keys = keys;
100
+ this.name = "EphemeralFlushError";
101
+ }
102
+ };
103
+ /**
104
+ * Redis-backed store for state that drives live queries but is never persisted
105
+ * to the database — a device's latest GPS fix, a "typing…" flag, a transient
106
+ * job progress percentage.
107
+ *
108
+ * Unlike a table row, an ephemeral key has no commit log and no snapshot ts, so
109
+ * it cannot participate in the OCC read set or the commit-log freshness replay.
110
+ * Instead a query records the plain key strings it read
111
+ * (`LiveResult.ephemeralKeys`), the sub-manager indexes subscriptions by those
112
+ * keys, and a write looks subscribers up by exact key match — cheaper than the
113
+ * write-set scan, because there is no predicate to evaluate.
114
+ */
115
+ var EphemeralStore = class {
116
+ cache;
117
+ defaultTtlSeconds;
118
+ /**
119
+ * Fired after a batch of writes lands in Redis, with the affected keys and the
120
+ * newly allocated global version. The server wires this to the sub-manager's
121
+ * ephemeral invalidation, mirroring how `SupaliveDb.setOnAfterCommit` drives
122
+ * write-set invalidation. Left unset outside a server (tests, tooling), where
123
+ * writes are then purely local.
124
+ */
125
+ onAfterWrite;
126
+ constructor(cache, opts) {
127
+ this.cache = cache;
128
+ this.defaultTtlSeconds = opts?.defaultTtlSeconds ?? 604800;
129
+ }
130
+ /** The TTL a `set` without an explicit `ttlSeconds` uses. */
131
+ get defaultTtl() {
132
+ return this.defaultTtlSeconds;
133
+ }
134
+ async find(key, schema) {
135
+ return decode(key, await this.cache.ephemeralGet(valueKeyOf(key)), schema);
136
+ }
137
+ async get(key, schema) {
138
+ const value = await this.find(key, schema);
139
+ if (value === null) throw new EphemeralKeyNotFoundError(key);
140
+ return value;
141
+ }
142
+ async getOr(key, schema, fallback) {
143
+ const value = await this.find(key, schema);
144
+ return value === null ? fallback : value;
145
+ }
146
+ async findMany(keys, schema) {
147
+ if (keys.length === 0) return [];
148
+ return (await this.cache.ephemeralGetMany(keys.map(valueKeyOf))).map((r, i) => decode(keys[i], r, schema));
149
+ }
150
+ async has(key) {
151
+ return await this.cache.ephemeralGet(valueKeyOf(key)) !== null;
152
+ }
153
+ async set(key, value, opts) {
154
+ await this.apply([this.prepareSet(key, value, opts)]);
155
+ }
156
+ async delete(key) {
157
+ await this.apply([this.prepareDelete(key)]);
158
+ }
159
+ /**
160
+ * Serialize a `set` into a buffered write WITHOUT touching Redis. Used by
161
+ * `DbWriter`, which defers ephemeral writes until its transaction commits.
162
+ */
163
+ prepareSet(key, value, opts) {
164
+ const ttlSeconds = opts?.ttlSeconds ?? this.defaultTtlSeconds;
165
+ if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) throw new Error(`Ephemeral ttlSeconds must be a positive number (got ${ttlSeconds})`);
166
+ return {
167
+ key,
168
+ value: supaliveStringify(value),
169
+ ttlSeconds: Math.ceil(ttlSeconds)
170
+ };
171
+ }
172
+ /** The `delete` counterpart of {@link prepareSet}. */
173
+ prepareDelete(key) {
174
+ return {
175
+ key,
176
+ value: "",
177
+ ttlSeconds: 0
178
+ };
179
+ }
180
+ /**
181
+ * Apply buffered writes atomically, then notify the invalidation hook. The
182
+ * hook runs only after Redis holds the new values, so any recompute it
183
+ * triggers is guaranteed to read them.
184
+ */
185
+ async apply(writes) {
186
+ if (writes.length === 0) return;
187
+ const version = await this.cache.evalScript(APPLY_EPHEMERAL_WRITES, [...writes.map((w) => valueKeyOf(w.key)), SEQ_KEY], writes.flatMap((w) => [w.value, String(w.ttlSeconds)]));
188
+ this.onAfterWrite?.(writes.map((w) => w.key), String(version));
189
+ }
190
+ };
191
+ function decode(key, raw, schema) {
192
+ if (raw === null || raw === void 0) return null;
193
+ let parsed;
194
+ try {
195
+ parsed = JSON.parse(raw);
196
+ } catch (cause) {
197
+ throw new EphemeralDecodeError(key, { cause });
198
+ }
199
+ const result = schema.safeParse(parsed);
200
+ if (!result.success) throw new EphemeralDecodeError(key, { cause: result.error });
201
+ return result.data;
202
+ }
203
+ /**
204
+ * An {@link EphemeralStore} backed by a plain Map instead of Redis — for unit
205
+ * tests and tooling that construct a `SupaliveDb` without a cache layer.
206
+ *
207
+ * It is process-local, so it is NOT a substitute for the real store in any
208
+ * multi-instance deployment: two servers would each see their own values, and
209
+ * cross-instance invalidation would push stale data. TTLs are honoured on read
210
+ * (there is no background expiry), which is enough for the semantics tests care
211
+ * about.
212
+ */
213
+ function createMemoryEphemeralStore(opts) {
214
+ const values = /* @__PURE__ */ new Map();
215
+ let seq = 0;
216
+ const read = (key) => {
217
+ const entry = values.get(key);
218
+ if (!entry) return null;
219
+ if (entry.expiresAt <= Date.now()) {
220
+ values.delete(key);
221
+ return null;
222
+ }
223
+ return entry.value;
224
+ };
225
+ return new EphemeralStore({
226
+ ephemeralGet: async (key) => read(key),
227
+ ephemeralGetMany: async (keys) => keys.map(read),
228
+ evalScript: async (_src, keys, args) => {
229
+ for (let i = 0; i < keys.length - 1; i++) {
230
+ const ttl = Number(args[i * 2 + 1]);
231
+ if (ttl <= 0) values.delete(keys[i]);
232
+ else values.set(keys[i], {
233
+ value: String(args[i * 2]),
234
+ expiresAt: Date.now() + ttl * 1e3
235
+ });
236
+ }
237
+ return ++seq;
238
+ }
239
+ }, opts);
240
+ }
241
+ /**
242
+ * Wrap `store` so every read also records a live-query dependency on the key it
243
+ * touched. The dependency is on the KEY, not the value: reading a key that is
244
+ * currently absent still subscribes you to it being set later.
245
+ */
246
+ function createTrackedEphemeralReader(ctx, store) {
247
+ return {
248
+ find: (key, schema) => {
249
+ ctx.internalTrackEphemeralRead(key);
250
+ return store.find(key, schema);
251
+ },
252
+ get: (key, schema) => {
253
+ ctx.internalTrackEphemeralRead(key);
254
+ return store.get(key, schema);
255
+ },
256
+ getOr: (key, schema, fallback) => {
257
+ ctx.internalTrackEphemeralRead(key);
258
+ return store.getOr(key, schema, fallback);
259
+ },
260
+ findMany: (keys, schema) => {
261
+ for (const key of keys) ctx.internalTrackEphemeralRead(key);
262
+ return store.findMany(keys, schema);
263
+ },
264
+ has: (key) => {
265
+ ctx.internalTrackEphemeralRead(key);
266
+ return store.has(key);
267
+ }
268
+ };
269
+ }
270
+ /**
271
+ * Wrap `store` for use inside a mutation: reads track as above, but writes are
272
+ * BUFFERED on the context and applied only once the transaction commits.
273
+ *
274
+ * Buffering is what makes ephemeral writes safe in a transactional handler. A
275
+ * mutation body can run several times — the OCC retry policy re-invokes it with
276
+ * a fresh `DbWriter` on every conflict — so writing to Redis eagerly would
277
+ * publish state from attempts that never committed, and push it to subscribers.
278
+ * Only the committing attempt's buffer is flushed; a losing attempt's buffer is
279
+ * discarded with its context.
280
+ */
281
+ function createBufferedEphemeralWriter(ctx, store) {
282
+ return {
283
+ ...createTrackedEphemeralReader(ctx, store),
284
+ set: async (key, value, opts) => {
285
+ ctx.internalBufferEphemeralWrite(store.prepareSet(key, value, opts));
286
+ },
287
+ delete: async (key) => {
288
+ ctx.internalBufferEphemeralWrite(store.prepareDelete(key));
289
+ }
290
+ };
291
+ }
292
+ //#endregion
293
+ export { EphemeralKeyNotFoundError as a, createMemoryEphemeralStore as c, EphemeralFlushError as i, createTrackedEphemeralReader as l, EPHEMERAL_DEFAULT_TTL_SECONDS as n, EphemeralStore as o, EphemeralDecodeError as r, createBufferedEphemeralWriter as s, EMPTY_EPHEMERAL_KEYS as t };
294
+
295
+ //# sourceMappingURL=ephemeral-CkidSG9y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ephemeral-CkidSG9y.js","names":[],"sources":["../src/db/ephemeral.ts"],"sourcesContent":["import type { ZodType } from \"zod\";\nimport type { CacheLayer } from \"./cache\";\nimport { supaliveStringify } from \"../helper\";\n\n/**\n * Default lifetime of an ephemeral entry: 7 days. Long enough that a value set\n * once (a device's last known position, a presence flag) survives normal\n * operation, short enough that abandoned keys eventually drain out of Redis on\n * their own. Override per call via {@link EphemeralSetOptions.ttlSeconds}.\n */\nexport const EPHEMERAL_DEFAULT_TTL_SECONDS = 7 * 24 * 60 * 60;\n\n/**\n * Shared stand-in for \"this computation read no ephemeral state\" — the\n * overwhelmingly common case. Frozen and reused so neither a `TxContext` nor a\n * `LiveResult` allocates an array for a query that never touches the store.\n */\nexport const EMPTY_EPHEMERAL_KEYS: string[] = Object.freeze([] as string[]) as string[];\n\n/** Redis key holding the value for an ephemeral entry. */\nfunction valueKeyOf(key: string): string {\n return `sl:eph:${key}`;\n}\n\n/**\n * Single global monotonic counter bumped by every ephemeral write. Its value is\n * the `version` handed to the invalidation path, where it plays exactly the role\n * a mutation's `commitTs` plays: the discriminator in the cross-instance\n * recompute lock (`lock:recompute:<subId>:<version>`), so every instance racing\n * to recompute the same write agrees on which write it is racing for.\n *\n * It deliberately is NOT per-key: a recompute always re-reads the current value\n * from Redis, so the version only has to be unique per write *event*, never a\n * meaningful per-key revision.\n */\nconst SEQ_KEY = \"sl:eph:seq\";\n\n/**\n * Apply a batch of ephemeral writes and return the new global sequence.\n *\n * KEYS[1..n] — value keys; KEYS[n+1] — the sequence key.\n * ARGV[2i-1] — serialized value for KEYS[i]; ARGV[2i] — its TTL in seconds,\n * where `0` means \"delete instead of set\".\n *\n * A single script keeps the whole batch atomic and costs one round trip no\n * matter how many keys a mutation buffered, and the trailing INCR allocates the\n * version *after* every value is visible — so a recompute triggered by this\n * version can never read a pre-write value.\n */\nconst APPLY_EPHEMERAL_WRITES = `\nlocal n = #KEYS - 1\nfor i = 1, n do\n local ttl = tonumber(ARGV[i * 2])\n if ttl <= 0 then\n redis.call('DEL', KEYS[i])\n else\n redis.call('SET', KEYS[i], ARGV[i * 2 - 1], 'EX', ttl)\n end\nend\nreturn redis.call('INCR', KEYS[n + 1])`;\n\n/** Thrown by {@link EphemeralReader.get} when the key holds no value. */\nexport class EphemeralKeyNotFoundError extends Error {\n constructor(readonly key: string) {\n super(`Ephemeral key not found: ${key}`);\n this.name = \"EphemeralKeyNotFoundError\";\n }\n}\n\n/**\n * Thrown when a stored value does not match the schema the reader asked for —\n * corrupt bytes, or (far more likely) a value written by an older deploy under\n * a shape that has since changed.\n *\n * This is a hard error rather than a silent `null` because the two are not the\n * same thing: `null` means \"nothing is here\", and quietly conflating a schema\n * mismatch with absence turns a deploy bug into a blank screen. A caller that\n * genuinely wants tolerance can express it in the schema itself — `.catch()`,\n * `.optional()`, or a union — which is strictly more expressive than anything\n * this layer could decide on its behalf.\n */\nexport class EphemeralDecodeError extends Error {\n constructor(readonly key: string, options?: { cause?: unknown }) {\n super(`Ephemeral value at \"${key}\" did not match the expected schema`, options);\n this.name = \"EphemeralDecodeError\";\n }\n}\n\n/**\n * Thrown when a mutation's buffered ephemeral writes could not be applied after\n * its transaction committed.\n *\n * The distinction this type exists to make: **the mutation succeeded**. Its rows\n * are written and durable, and their subscription invalidation has already gone\n * out. Only the ephemeral half — which lives in Redis, outside the transaction —\n * failed to land. A caller must NOT respond by retrying the mutation; that would\n * apply the row writes a second time. Retry the ephemeral write alone, or let\n * the next write to those keys carry the state forward.\n */\nexport class EphemeralFlushError extends Error {\n constructor(readonly keys: string[], options?: { cause?: unknown }) {\n super(\n `Mutation committed, but its ephemeral writes failed to apply (keys: ${keys.join(\", \")}). ` +\n `The database changes are durable — do not retry the mutation.`,\n options,\n );\n this.name = \"EphemeralFlushError\";\n }\n}\n\nexport interface EphemeralSetOptions {\n /**\n * Lifetime in seconds. Defaults to {@link EPHEMERAL_DEFAULT_TTL_SECONDS}\n * (7 days), or to the server-configured default.\n *\n * Expiry is silent: when the TTL elapses the value simply disappears and the\n * next recompute of a subscribed query observes `null`. It does NOT by itself\n * push an update — call {@link EphemeralWriter.delete} when subscribers need\n * to see the value go away at a specific moment.\n */\n ttlSeconds?: number;\n}\n\n/**\n * Read side of the ephemeral store.\n *\n * Reached from a query/mutation handler as `ctx.db.ephemeral`, where every read\n * also records a live-query dependency on the key, and from an action/job as\n * `ctx.db.ephemeral` on the {@link SupaliveDb} itself (untracked — an action has\n * no read set).\n *\n * Every read takes the Zod schema its value should satisfy. Ephemeral state is\n * schemaless in Redis — no table, no column types, and its writer may be a\n * different deploy of your own code — so a `as T` cast here would be a type\n * that is checked nowhere and true only by convention. Passing the schema makes\n * the boundary an actual parse.\n *\n * The naming mirrors the SQL query builder: {@link find} returns `null` for a\n * missing key exactly like `query(Schema).find(id)` does, {@link get} is the\n * \"I know it's there\" variant that throws, and {@link getOr} supplies a default.\n */\nexport interface EphemeralReader {\n /** Value for `key`, or `null` when it is absent or expired. */\n find<T>(key: string, schema: ZodType<T>): Promise<T | null>;\n /** Value for `key`. Throws {@link EphemeralKeyNotFoundError} when absent. */\n get<T>(key: string, schema: ZodType<T>): Promise<T>;\n /** Value for `key`, or `fallback` when it is absent or expired. */\n getOr<T>(key: string, schema: ZodType<T>, fallback: T): Promise<T>;\n /** Values for `keys`, positionally aligned; `null` for each absent key. */\n findMany<T>(keys: string[], schema: ZodType<T>): Promise<(T | null)[]>;\n /** Whether `key` currently holds a value. Does not decode it, so it needs no\n * schema — and a value that fails to parse still counts as present. */\n has(key: string): Promise<boolean>;\n}\n\n/** Read side plus writes. See {@link EphemeralReader} for the read semantics. */\nexport interface EphemeralWriter extends EphemeralReader {\n /** Store `value` under `key`, replacing any existing entry. */\n set<T>(key: string, value: T, opts?: EphemeralSetOptions): Promise<void>;\n /** Remove `key`. Invalidates subscribers just like a `set` does. */\n delete(key: string): Promise<void>;\n}\n\n/** A single pending write in a `DbWriter`'s buffer. `ttlSeconds: 0` = delete. */\nexport interface PendingEphemeralWrite {\n key: string;\n /** Pre-serialized payload; empty string for a delete. */\n value: string;\n ttlSeconds: number;\n}\n\n/**\n * Redis-backed store for state that drives live queries but is never persisted\n * to the database — a device's latest GPS fix, a \"typing…\" flag, a transient\n * job progress percentage.\n *\n * Unlike a table row, an ephemeral key has no commit log and no snapshot ts, so\n * it cannot participate in the OCC read set or the commit-log freshness replay.\n * Instead a query records the plain key strings it read\n * (`LiveResult.ephemeralKeys`), the sub-manager indexes subscriptions by those\n * keys, and a write looks subscribers up by exact key match — cheaper than the\n * write-set scan, because there is no predicate to evaluate.\n */\nexport class EphemeralStore implements EphemeralWriter {\n private readonly cache: CacheLayer;\n private readonly defaultTtlSeconds: number;\n\n /**\n * Fired after a batch of writes lands in Redis, with the affected keys and the\n * newly allocated global version. The server wires this to the sub-manager's\n * ephemeral invalidation, mirroring how `SupaliveDb.setOnAfterCommit` drives\n * write-set invalidation. Left unset outside a server (tests, tooling), where\n * writes are then purely local.\n */\n onAfterWrite?: (keys: string[], version: string) => void;\n\n constructor(cache: CacheLayer, opts?: { defaultTtlSeconds?: number }) {\n this.cache = cache;\n this.defaultTtlSeconds = opts?.defaultTtlSeconds ?? EPHEMERAL_DEFAULT_TTL_SECONDS;\n }\n\n /** The TTL a `set` without an explicit `ttlSeconds` uses. */\n get defaultTtl(): number {\n return this.defaultTtlSeconds;\n }\n\n async find<T>(key: string, schema: ZodType<T>): Promise<T | null> {\n const raw = await this.cache.ephemeralGet(valueKeyOf(key));\n return decode(key, raw, schema);\n }\n\n async get<T>(key: string, schema: ZodType<T>): Promise<T> {\n const value = await this.find(key, schema);\n if (value === null) throw new EphemeralKeyNotFoundError(key);\n return value;\n }\n\n async getOr<T>(key: string, schema: ZodType<T>, fallback: T): Promise<T> {\n const value = await this.find(key, schema);\n return value === null ? fallback : value;\n }\n\n async findMany<T>(keys: string[], schema: ZodType<T>): Promise<(T | null)[]> {\n if (keys.length === 0) return [];\n const raw = await this.cache.ephemeralGetMany(keys.map(valueKeyOf));\n return raw.map((r, i) => decode(keys[i]!, r, schema));\n }\n\n async has(key: string): Promise<boolean> {\n return (await this.cache.ephemeralGet(valueKeyOf(key))) !== null;\n }\n\n async set<T>(key: string, value: T, opts?: EphemeralSetOptions): Promise<void> {\n await this.apply([this.prepareSet(key, value, opts)]);\n }\n\n async delete(key: string): Promise<void> {\n await this.apply([this.prepareDelete(key)]);\n }\n\n /**\n * Serialize a `set` into a buffered write WITHOUT touching Redis. Used by\n * `DbWriter`, which defers ephemeral writes until its transaction commits.\n */\n prepareSet<T>(key: string, value: T, opts?: EphemeralSetOptions): PendingEphemeralWrite {\n const ttlSeconds = opts?.ttlSeconds ?? this.defaultTtlSeconds;\n if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {\n throw new Error(`Ephemeral ttlSeconds must be a positive number (got ${ttlSeconds})`);\n }\n return { key, value: supaliveStringify(value), ttlSeconds: Math.ceil(ttlSeconds) };\n }\n\n /** The `delete` counterpart of {@link prepareSet}. */\n prepareDelete(key: string): PendingEphemeralWrite {\n return { key, value: \"\", ttlSeconds: 0 };\n }\n\n /**\n * Apply buffered writes atomically, then notify the invalidation hook. The\n * hook runs only after Redis holds the new values, so any recompute it\n * triggers is guaranteed to read them.\n */\n async apply(writes: PendingEphemeralWrite[]): Promise<void> {\n if (writes.length === 0) return;\n\n const version = await this.cache.evalScript(\n APPLY_EPHEMERAL_WRITES,\n [...writes.map((w) => valueKeyOf(w.key)), SEQ_KEY],\n writes.flatMap((w) => [w.value, String(w.ttlSeconds)]),\n );\n\n this.onAfterWrite?.(writes.map((w) => w.key), String(version));\n }\n}\n\nfunction decode<T>(key: string, raw: string | null, schema: ZodType<T>): T | null {\n if (raw === null || raw === undefined) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (cause) {\n throw new EphemeralDecodeError(key, { cause });\n }\n\n const result = schema.safeParse(parsed);\n if (!result.success) throw new EphemeralDecodeError(key, { cause: result.error });\n return result.data;\n}\n\n/**\n * An {@link EphemeralStore} backed by a plain Map instead of Redis — for unit\n * tests and tooling that construct a `SupaliveDb` without a cache layer.\n *\n * It is process-local, so it is NOT a substitute for the real store in any\n * multi-instance deployment: two servers would each see their own values, and\n * cross-instance invalidation would push stale data. TTLs are honoured on read\n * (there is no background expiry), which is enough for the semantics tests care\n * about.\n */\nexport function createMemoryEphemeralStore(opts?: { defaultTtlSeconds?: number }): EphemeralStore {\n const values = new Map<string, { value: string; expiresAt: number }>();\n let seq = 0;\n\n const read = (key: string): string | null => {\n const entry = values.get(key);\n if (!entry) return null;\n if (entry.expiresAt <= Date.now()) {\n values.delete(key);\n return null;\n }\n return entry.value;\n };\n\n const backing = {\n ephemeralGet: async (key: string) => read(key),\n ephemeralGetMany: async (keys: string[]) => keys.map(read),\n // Mirrors APPLY_EPHEMERAL_WRITES: the trailing key is the sequence, and each\n // value key carries a (value, ttl) ARGV pair where ttl <= 0 means delete.\n evalScript: async (_src: string, keys: string[], args: (string | number)[]) => {\n for (let i = 0; i < keys.length - 1; i++) {\n const ttl = Number(args[i * 2 + 1]);\n if (ttl <= 0) values.delete(keys[i]!);\n else values.set(keys[i]!, { value: String(args[i * 2]), expiresAt: Date.now() + ttl * 1000 });\n }\n return ++seq;\n },\n } as CacheLayer;\n\n return new EphemeralStore(backing, opts);\n}\n\n// ─── Handler-facing wrappers ─────────────────────────────────────────────────\n//\n// Built as free functions over a context rather than methods on it: the tracking\n// and buffering they add is the ONLY difference from the bare store, and keeping\n// them out of `DbReader`/`DbWriter` leaves those classes about SQL. Each takes\n// the context it reports to as its first argument.\n\n/** The slice of a `DbReader` these wrappers drive. Structural, so `context.ts`\n * doesn't have to be imported here (it imports this module). */\nexport interface EphemeralReadTracker {\n internalTrackEphemeralRead(key: string): void;\n}\n\n/** The slice of a `DbWriter` the write wrapper drives. */\nexport interface EphemeralWriteBuffer extends EphemeralReadTracker {\n internalBufferEphemeralWrite(write: PendingEphemeralWrite): void;\n}\n\n/**\n * Wrap `store` so every read also records a live-query dependency on the key it\n * touched. The dependency is on the KEY, not the value: reading a key that is\n * currently absent still subscribes you to it being set later.\n */\nexport function createTrackedEphemeralReader(\n ctx: EphemeralReadTracker,\n store: EphemeralStore,\n): EphemeralReader {\n return {\n find: (key, schema) => {\n ctx.internalTrackEphemeralRead(key);\n return store.find(key, schema);\n },\n get: (key, schema) => {\n ctx.internalTrackEphemeralRead(key);\n return store.get(key, schema);\n },\n getOr: (key, schema, fallback) => {\n ctx.internalTrackEphemeralRead(key);\n return store.getOr(key, schema, fallback);\n },\n findMany: (keys, schema) => {\n for (const key of keys) ctx.internalTrackEphemeralRead(key);\n return store.findMany(keys, schema);\n },\n has: (key) => {\n ctx.internalTrackEphemeralRead(key);\n return store.has(key);\n },\n };\n}\n\n/**\n * Wrap `store` for use inside a mutation: reads track as above, but writes are\n * BUFFERED on the context and applied only once the transaction commits.\n *\n * Buffering is what makes ephemeral writes safe in a transactional handler. A\n * mutation body can run several times — the OCC retry policy re-invokes it with\n * a fresh `DbWriter` on every conflict — so writing to Redis eagerly would\n * publish state from attempts that never committed, and push it to subscribers.\n * Only the committing attempt's buffer is flushed; a losing attempt's buffer is\n * discarded with its context.\n */\nexport function createBufferedEphemeralWriter(\n ctx: EphemeralWriteBuffer,\n store: EphemeralStore,\n): EphemeralWriter {\n return {\n ...createTrackedEphemeralReader(ctx, store),\n set: async (key, value, opts) => {\n ctx.internalBufferEphemeralWrite(store.prepareSet(key, value, opts));\n },\n delete: async (key) => {\n ctx.internalBufferEphemeralWrite(store.prepareDelete(key));\n },\n };\n}\n"],"mappings":"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"}