@ultimat3/realtime 23.0.0 → 25.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CLAUDE.md +118 -126
  2. package/README.md +229 -54
  3. package/package.json +9 -6
  4. package/src/advisory-lock.ts +62 -0
  5. package/src/boot.ts +25 -15
  6. package/src/change-buffer.ts +96 -41
  7. package/src/changefeed-env.ts +9 -9
  8. package/src/changefeed.ts +81 -19
  9. package/src/channel-authz.ts +48 -17
  10. package/src/channel-describe.ts +1 -1
  11. package/src/channel-latch.ts +52 -0
  12. package/src/channel-logs.ts +36 -15
  13. package/src/channel-presence.ts +3 -3
  14. package/src/channel-records.ts +35 -4
  15. package/src/channel-ref.ts +2 -2
  16. package/src/channel-registry.ts +2 -2
  17. package/src/channel-sids.ts +37 -0
  18. package/src/channel.ts +148 -48
  19. package/src/client-channels.ts +12 -1
  20. package/src/client-contract.ts +4 -1
  21. package/src/client-frames.ts +16 -3
  22. package/src/client-heartbeat.ts +39 -9
  23. package/src/client.ts +30 -2
  24. package/src/cursor.ts +11 -0
  25. package/src/error-titles.ts +91 -0
  26. package/src/errors.ts +113 -105
  27. package/src/fanout.ts +13 -0
  28. package/src/first-paint-hold.ts +49 -0
  29. package/src/idb-types.ts +2 -2
  30. package/src/index.ts +25 -7
  31. package/src/json.ts +16 -3
  32. package/src/live-contract.ts +14 -0
  33. package/src/live-definition.ts +34 -9
  34. package/src/live-deliver.ts +55 -0
  35. package/src/live-fanout.ts +122 -29
  36. package/src/live-floor.ts +35 -0
  37. package/src/live-query-options.ts +44 -0
  38. package/src/live-query.ts +96 -175
  39. package/src/live-reauth.ts +127 -0
  40. package/src/live-replicator.ts +28 -8
  41. package/src/live-resume.ts +44 -8
  42. package/src/live-rows.ts +32 -3
  43. package/src/live-spend.ts +50 -0
  44. package/src/local-store-idb.ts +5 -0
  45. package/src/matcher-bridge.ts +11 -5
  46. package/src/nats-client.ts +37 -6
  47. package/src/nats-lib-client.ts +36 -7
  48. package/src/nats-subscriptions.ts +63 -0
  49. package/src/nats-transport.ts +149 -15
  50. package/src/offline-queue.ts +53 -5
  51. package/src/page-errors.ts +25 -9
  52. package/src/page-outbox.ts +125 -54
  53. package/src/page-runtime-wait.ts +72 -0
  54. package/src/page-runtime.ts +32 -0
  55. package/src/page-socket.ts +7 -16
  56. package/src/page-store.ts +83 -16
  57. package/src/pg-advisory-lock.ts +54 -5
  58. package/src/pg-auth.ts +9 -4
  59. package/src/pg-bytes.ts +4 -1
  60. package/src/pg-connection.ts +126 -22
  61. package/src/pg-entity-row.ts +16 -0
  62. package/src/pg-identity-tables.ts +25 -0
  63. package/src/pg-preflight.ts +50 -4
  64. package/src/pg-replication-row.ts +82 -0
  65. package/src/pg-replication.ts +50 -51
  66. package/src/pg-socket.ts +26 -7
  67. package/src/pg-tls.ts +18 -0
  68. package/src/pg-values.ts +29 -14
  69. package/src/pg-wire.ts +5 -3
  70. package/src/pgoutput.ts +19 -9
  71. package/src/policy-gate.ts +7 -7
  72. package/src/presence.ts +87 -10
  73. package/src/principal-sockets.ts +75 -0
  74. package/src/query-window.ts +17 -5
  75. package/src/record-persister.ts +7 -2
  76. package/src/record-publisher.ts +178 -0
  77. package/src/record-store.ts +25 -4
  78. package/src/record-synced.ts +43 -1
  79. package/src/replication-errors.ts +11 -7
  80. package/src/replicator-envelope.ts +160 -0
  81. package/src/replicator-row.ts +75 -0
  82. package/src/replicator.ts +313 -191
  83. package/src/server.ts +45 -23
  84. package/src/socket-defaults.ts +20 -0
  85. package/src/socket-engine.ts +14 -2
  86. package/src/socket-idle.ts +3 -1
  87. package/src/socket-routes.ts +8 -3
  88. package/src/socket.ts +14 -19
  89. package/src/subscriber-gate.ts +11 -1
  90. package/src/subscription-book.ts +129 -71
  91. package/src/subscription-count.ts +151 -0
  92. package/src/sync-actor-change.ts +56 -0
  93. package/src/sync-auth.ts +47 -2
  94. package/src/sync-bus-handlers.ts +131 -0
  95. package/src/sync-frames.ts +74 -21
  96. package/src/sync-node-bounds.ts +16 -1
  97. package/src/sync-node-contract.ts +36 -4
  98. package/src/sync-node.ts +163 -82
  99. package/src/sync-origin.ts +27 -12
  100. package/src/sync-protocol.ts +86 -16
  101. package/src/sync-upgrade.ts +115 -8
  102. package/src/type-pins.ts +9 -1
  103. package/src/use-channel.ts +19 -14
  104. package/src/use-connection.ts +3 -3
  105. package/src/use-mutation.ts +41 -20
  106. package/src/use-query.ts +11 -11
  107. package/src/use-record.ts +89 -10
  108. package/src/wire-channel.ts +18 -9
  109. package/src/wire-read.ts +2 -2
  110. package/src/idb-fake.ts +0 -133
  111. package/src/policy-fake.ts +0 -14
package/src/use-query.ts CHANGED
@@ -1,14 +1,12 @@
1
- // The ONE read hook. Live-ness is the query's, not the hook's: a live ref subscribes over the
2
- // page socket, any other is one HTTP read through `@ultimat3/query`'s client (and so through
3
- // core's one transport). Either way a list is an ORDER of keys and the rows are the page store's,
4
- // so a record updated anywhere re-renders every list showing it without refetching the list.
1
+ // The ONE read hook. Live-ness is the query's: a live ref subscribes over the page socket, any other
2
+ // is one HTTP read through `@ultimat3/query`'s client — both the page runtime's, never bundled here.
3
+ // Either way a list is an ORDER of keys and the rows are the page store's, so a record updated
4
+ // anywhere re-renders every list showing it without refetching the list.
5
5
 
6
6
  import { type AsyncState, isSuperseded, type RecordEnvelope, type Row } from '@ultimat3/core/page';
7
- import { queryClientMethodFor } from '@ultimat3/query/client';
8
7
  import type { LiveHandle } from './client-contract';
9
8
  import type { JsonValue } from './json';
10
- import { pageSocket } from './page-socket';
11
- import { pageStore } from './page-store';
9
+ import { installedPage, type PageServices } from './page-store';
12
10
  import { isServerRender, signalFor } from './reactivity';
13
11
  import { type RecordStore, recordKey } from './record-store';
14
12
 
@@ -71,11 +69,12 @@ export function useQuery<R extends object = Row>(
71
69
  [Symbol.dispose]: nothing,
72
70
  });
73
71
  }
72
+ const page = installedPage('useQuery');
74
73
  const [version, setVersion] = signal(0);
75
74
  const bump = (): void => setVersion(version() + 1);
76
75
  return ref.live === true
77
- ? liveAccessor<R>(pageSocket('useQuery').subscribeLive<R>(ref, input), version, bump)
78
- : readAccessor<R>(pageStore(), ref, input, options, version, bump);
76
+ ? liveAccessor<R>(page.services.socket('useQuery').subscribeLive<R>(ref, input), version, bump)
77
+ : readAccessor<R>(page.store, page.services.read(ref.name), ref, input, options, version, bump);
79
78
  }
80
79
 
81
80
  function liveAccessor<R extends object>(
@@ -115,6 +114,7 @@ function liveAccessor<R extends object>(
115
114
 
116
115
  function readAccessor<R extends object>(
117
116
  store: RecordStore,
117
+ method: ReturnType<PageServices['read']>,
118
118
  ref: QueryRef,
119
119
  input: JsonValue,
120
120
  options: QueryOptions,
@@ -122,7 +122,6 @@ function readAccessor<R extends object>(
122
122
  bump: () => void,
123
123
  ): QueryAccessor<R> {
124
124
  const type = ref.entity;
125
- const method = queryClientMethodFor(ref.name, { baseUrl: '' });
126
125
  let state: AsyncState<readonly Row[]> = PENDING;
127
126
  /** Record keys in answer order when the rows are records; the rows themselves when not. */
128
127
  let keys: readonly string[] = [];
@@ -177,9 +176,10 @@ function readAccessor<R extends object>(
177
176
  append && after !== null ? { first: options.first, after } : { first: options.first };
178
177
  // The page's cursor travels WITH its rows and is taken only where the rows are: a `more()`
179
178
  // a refetch superseded wrote its cursor here, before the generation check discarded its rows.
179
+ // `nextCursor` is `null` exactly when `hasMore` is false (core's `Page`), so it IS the answer.
180
180
  return method.page(input, controls, { onEnvelope }).then((page) => ({
181
181
  ...answered(page.rows as readonly Row[]),
182
- next: page.hasNextPage ? page.endCursor : null,
182
+ next: page.nextCursor,
183
183
  }));
184
184
  };
185
185
 
package/src/use-record.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  // One record, by type and key, out of the page's store — the same object every island showing it
2
2
  // holds, moved once by whichever write lands first (an HTTP answer, a socket frame, an optimistic
3
- // twin). Imports no socket: an island that only reads records ships none of the lifecycle.
3
+ // twin). A reader of the page runtime's store: an island that only reads records ships the hook and
4
+ // none of the store, the socket or its lifecycle.
4
5
 
5
6
  import type { AsyncState, Row } from '@ultimat3/core/page';
6
7
  import { pageStore } from './page-store';
7
8
  import { isServerRender, signalFor } from './reactivity';
8
- import { recordKey } from './record-store';
9
+ import { type RecordStore, recordKey } from './record-store';
9
10
 
10
11
  /**
11
12
  * A callable `AsyncState`: `pending` until the record first arrives, `ready` from then on —
@@ -31,7 +32,7 @@ export function useRecord<R extends object = Row>(type: string, key: string): Re
31
32
  [Symbol.dispose]: releaseNothing,
32
33
  });
33
34
  }
34
- const store = pageStore();
35
+ const store = pageStore('useRecord');
35
36
  const [version, setVersion] = signal(0);
36
37
  const target = recordKey(type, key);
37
38
  store.retain(type, key);
@@ -59,20 +60,37 @@ export function useRecord<R extends object = Row>(type: string, key: string): Re
59
60
  return Object.assign(read, { release, [Symbol.dispose]: release });
60
61
  }
61
62
 
62
- /** A callable `AsyncState` over several records, in the order their keys were given. */
63
+ /** A callable `AsyncState` over several records of one type. */
63
64
  export type RecordsAccessor<R extends object = Row> = (() => AsyncState<readonly R[]>) & {
64
65
  release(): void;
65
66
  [Symbol.dispose](): void;
66
67
  };
67
68
 
68
69
  /**
69
- * Several records of one type, by key — the same store objects `useRecord` hands out. `pending`
70
- * until the first of them arrives; then `ready` with those present, in key order: a record the
71
- * server removed simply drops out, and a list is never shown as its skeleton again.
70
+ * Which records of a type a whole-type `useRecords` shows, and in what order. Both run on every
71
+ * change to that type, in the browser, over rows already on the page: pure, and cheap.
72
+ */
73
+ export interface RecordSelection<R extends object = Row> {
74
+ /** Keep a record only when this answers `true`. Absent: every record of the type. */
75
+ readonly where?: (record: R) => boolean;
76
+ /** Absent: by record key, ascending — creation order for a uuid v7 key. */
77
+ readonly order?: (a: R, b: R) => number;
78
+ }
79
+
80
+ /**
81
+ * Records of one type out of the page store — the same objects `useRecord` hands out.
82
+ *
83
+ * - **By key** (`useRecords('posts', keys)`): `pending` until the first of them arrives, then
84
+ * `ready` with those present, in key order; a record the server removed drops out.
85
+ * - **The whole type** (`useRecords('runs', {})`, `useRecords('runs', { where, order })`): every record
86
+ * of it the store holds — HTTP answers, live patches, a channel's `records` frames and pending
87
+ * optimistic writes alike — `ready` from the first read (an empty store is an empty list; whether
88
+ * the page has caught up is the channel's or the query's to say). A record a frame adopts joins
89
+ * the list; none is evicted while it is held. The channel-records counterpart of a live window.
72
90
  */
73
91
  export function useRecords<R extends object = Row>(
74
92
  type: string,
75
- keys: readonly string[],
93
+ select: readonly string[] | RecordSelection<R>,
76
94
  ): RecordsAccessor<R> {
77
95
  const signal = signalFor('useRecords');
78
96
  if (isServerRender()) {
@@ -81,8 +99,24 @@ export function useRecords<R extends object = Row>(
81
99
  [Symbol.dispose]: releaseNothing,
82
100
  });
83
101
  }
84
- const store = pageStore();
102
+ const store = pageStore('useRecords');
85
103
  const [version, setVersion] = signal(0);
104
+ const bump = (): void => setVersion(version() + 1);
105
+ return isKeyList(select)
106
+ ? byKeys<R>(store, type, select, version, bump)
107
+ : ofType<R>(store, type, select, version, bump);
108
+ }
109
+
110
+ // `Array.isArray` narrows a `readonly` array union to `any[]`; this keeps the element type.
111
+ const isKeyList = (value: unknown): value is readonly string[] => Array.isArray(value);
112
+
113
+ function byKeys<R extends object>(
114
+ store: RecordStore,
115
+ type: string,
116
+ keys: readonly string[],
117
+ version: () => number,
118
+ bump: () => void,
119
+ ): RecordsAccessor<R> {
86
120
  const targets = new Set(keys.map((key) => recordKey(type, key)));
87
121
  const present = (): R[] => {
88
122
  const out: R[] = [];
@@ -100,7 +134,7 @@ export function useRecords<R extends object = Row>(
100
134
  const unsubscribe = store.subscribe((changed) => {
101
135
  if (![...changed].some((key) => targets.has(key))) return;
102
136
  if (present().length > 0) seen = true;
103
- setVersion(version() + 1);
137
+ bump();
104
138
  });
105
139
  const read = (): AsyncState<readonly R[]> => {
106
140
  version();
@@ -119,3 +153,48 @@ export function useRecords<R extends object = Row>(
119
153
  };
120
154
  return Object.assign(read, { release, [Symbol.dispose]: release });
121
155
  }
156
+
157
+ function ofType<R extends object>(
158
+ store: RecordStore,
159
+ type: string,
160
+ selection: RecordSelection<R>,
161
+ version: () => number,
162
+ bump: () => void,
163
+ ): RecordsAccessor<R> {
164
+ const prefix = `${type}:`;
165
+ const { where, order } = selection;
166
+ store.retainType(type);
167
+ // Recomputed once per change to this type, never per read: the same answer back is what tells
168
+ // a fine-grained renderer nothing moved.
169
+ let answer: AsyncState<readonly R[]> | null = null;
170
+ let answered = -1;
171
+ const unsubscribe = store.subscribe((changed) => {
172
+ for (const rk of changed) {
173
+ if (!rk.startsWith(prefix)) continue;
174
+ bump();
175
+ return;
176
+ }
177
+ });
178
+ const read = (): AsyncState<readonly R[]> => {
179
+ const at = version();
180
+ if (answer !== null && answered === at) return answer;
181
+ // The store holds JSON rows; the caller names the entity row type it reads them as.
182
+ const keyed = store.keyed(type) as readonly (readonly [string, R])[];
183
+ const kept = where === undefined ? [...keyed] : keyed.filter(([, row]) => where(row));
184
+ const rows =
185
+ order === undefined
186
+ ? kept.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([, row]) => row)
187
+ : kept.map(([, row]) => row).sort(order);
188
+ answer = { status: 'ready', data: rows };
189
+ answered = at;
190
+ return answer;
191
+ };
192
+ let held = true;
193
+ const release = (): void => {
194
+ if (!held) return;
195
+ held = false;
196
+ unsubscribe();
197
+ store.releaseType(type);
198
+ };
199
+ return Object.assign(read, { release, [Symbol.dispose]: release });
200
+ }
@@ -12,7 +12,7 @@ import type {
12
12
  ChannelSubscribeTarget,
13
13
  ReplayGapFrame,
14
14
  } from './channel-wire';
15
- import { isJsonObject, type JsonObject } from './json';
15
+ import { isParsedJsonObject, type JsonObject } from './json';
16
16
  import { fail, list, num, object, str } from './wire-read';
17
17
  import { FRAME_LIMITS, PROTOCOL_VERSION } from './wire-version';
18
18
 
@@ -65,7 +65,7 @@ export function channelTarget(value: JsonObject): ChannelSubscribeTarget {
65
65
  `channel params carry ${entries.length}, over the limit of ${FRAME_LIMITS.channelParams}`,
66
66
  );
67
67
  }
68
- const params: Record<string, string> = {};
68
+ const params = bare<string>();
69
69
  for (const [key, param] of entries) {
70
70
  if (typeof param !== 'string') throw fail(`channel param "${key}" must be a string`);
71
71
  params[key] = param;
@@ -77,7 +77,7 @@ export function channelTarget(value: JsonObject): ChannelSubscribeTarget {
77
77
  }
78
78
 
79
79
  function sinceOf(value: unknown): ChannelSince {
80
- if (!isJsonObject(value)) throw fail('channel since must be an object');
80
+ if (!isParsedJsonObject(value)) throw fail('channel since must be an object');
81
81
  const seq = num(value, 'seq');
82
82
  if (!Number.isInteger(seq) || seq < 0) throw fail('channel since.seq must be a whole number');
83
83
  return { epoch: str(value, 'epoch'), seq };
@@ -85,12 +85,12 @@ function sinceOf(value: unknown): ChannelSince {
85
85
 
86
86
  /** type → key → row. Every row an object, and the whole frame under the snapshot row ceiling. */
87
87
  function adoptOf(value: unknown): ChannelAdopt {
88
- if (!isJsonObject(value)) throw fail('records.adopt must be an object');
88
+ if (!isParsedJsonObject(value)) throw fail('records.adopt must be an object');
89
89
  let total = 0;
90
- const out: Record<string, Readonly<Record<string, Row>>> = {};
90
+ const out = bare<Readonly<Record<string, Row>>>();
91
91
  for (const [type, keyed] of Object.entries(value)) {
92
- if (!isJsonObject(keyed)) throw fail(`records.adopt.${type} must be an object`);
93
- const rows: Record<string, Row> = {};
92
+ if (!isParsedJsonObject(keyed)) throw fail(`records.adopt.${type} must be an object`);
93
+ const rows = bare<Row>();
94
94
  for (const [key, row] of Object.entries(keyed)) {
95
95
  total += 1;
96
96
  if (total > FRAME_LIMITS.rows) {
@@ -104,8 +104,8 @@ function adoptOf(value: unknown): ChannelAdopt {
104
104
  }
105
105
 
106
106
  function removeOf(value: unknown): ChannelRemove {
107
- if (!isJsonObject(value)) throw fail('records.remove must be an object');
108
- const out: Record<string, readonly string[]> = {};
107
+ if (!isParsedJsonObject(value)) throw fail('records.remove must be an object');
108
+ const out = bare<readonly string[]>();
109
109
  for (const type of Object.keys(value)) {
110
110
  out[type] = list(value, type, FRAME_LIMITS.rows, `records.remove.${type}`).map((key) => {
111
111
  if (typeof key !== 'string') throw fail(`records.remove.${type} must hold strings`);
@@ -114,3 +114,12 @@ function removeOf(value: unknown): ChannelRemove {
114
114
  }
115
115
  return out;
116
116
  }
117
+
118
+ /**
119
+ * A map keyed by WIRE data — a record type, a record key, a channel param. Null-prototype, as the
120
+ * encoder's are (`channel-render.ts`): on a plain object `out['__proto__'] = rows` sets the map's
121
+ * prototype instead of a key, so that entry was dropped and every other read went through it.
122
+ */
123
+ function bare<V>(): Record<string, V> {
124
+ return Object.create(null) as Record<string, V>;
125
+ }
package/src/wire-read.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // refusal is `X_PROTOCOL_VERSION` with the field named. Shared by `sync-protocol.ts` and
3
3
  // `wire-channel.ts`, so the channel frames are held to the same ceilings as every other kind.
4
4
 
5
- import { isJsonObject, type JsonObject, type JsonValue } from './json';
5
+ import { isParsedJsonObject, type JsonObject, type JsonValue } from './json';
6
6
  import { ProtocolVersionError } from './page-errors';
7
7
  import { FRAME_LIMITS, PROTOCOL_VERSION } from './wire-version';
8
8
 
@@ -81,6 +81,6 @@ export function bounded(value: JsonValue, label: string): JsonValue {
81
81
  }
82
82
 
83
83
  export function object(value: unknown): JsonObject {
84
- if (!isJsonObject(value)) throw fail('expected a JSON object');
84
+ if (!isParsedJsonObject(value)) throw fail('expected a JSON object');
85
85
  return value;
86
86
  }
package/src/idb-fake.ts DELETED
@@ -1,133 +0,0 @@
1
- /**
2
- * An in-memory IndexedDB with exactly the surface `local-store-idb.ts` uses — open with an
3
- * upgrade, one transaction over named stores, `put` / `delete` / `getAll` / `getAllKeys` — and
4
- * IndexedDB's own asynchrony: every request answers on a later microtask and a transaction
5
- * completes after its last request. Bun ships no IndexedDB, and no dependency is worth a test seam.
6
- * Values are structured-cloned in and out, as the real one does.
7
- */
8
-
9
- import type { IdbDatabaseLike, IdbFactoryLike, IdbRequestLike, IdbStoreLike } from './idb-types';
10
-
11
- type Tables = Map<string, Map<string, unknown>>;
12
-
13
- export interface FakeIdbOptions {
14
- /** `open` fails, as it does in a private window or with storage blocked. */
15
- readonly blocked?: boolean;
16
- /**
17
- * Every write ABORTS its transaction, as a quota refusal does: `abort` fires and nothing else —
18
- * no `complete`, no transaction `error` event. Read at write time, so a test can flip it.
19
- */
20
- quotaExceeded?: boolean;
21
- }
22
-
23
- /** A fresh, empty database server. Share one instance to simulate a reload over the same disk. */
24
- export function fakeIndexedDb(options: FakeIdbOptions = {}): IdbFactoryLike {
25
- const databases = new Map<string, { version: number; tables: Tables }>();
26
- return {
27
- open(name: string, version: number): IdbRequestLike<IdbDatabaseLike> {
28
- const request = pendingRequest<IdbDatabaseLike>();
29
- queueMicrotask(() => {
30
- if (options.blocked === true) {
31
- request.fail(new DOMException('The operation is insecure.', 'SecurityError'));
32
- return;
33
- }
34
- const known = databases.get(name) ?? { version: 0, tables: new Map() };
35
- databases.set(name, known);
36
- const db = database(known.tables, options);
37
- request.result = db;
38
- if (known.version < version) {
39
- known.version = version;
40
- request.onupgradeneeded?.();
41
- }
42
- request.onsuccess?.();
43
- });
44
- return request;
45
- },
46
- };
47
- }
48
-
49
- function database(tables: Tables, options: FakeIdbOptions): IdbDatabaseLike {
50
- return {
51
- objectStoreNames: { contains: (name: string): boolean => tables.has(name) },
52
- createObjectStore: (name: string): void => {
53
- tables.set(name, new Map());
54
- },
55
- transaction(names: string | readonly string[]) {
56
- let open = 0;
57
- let failed = false;
58
- const tx = {
59
- oncomplete: null as (() => void) | null,
60
- onerror: null as (() => void) | null,
61
- onabort: null as (() => void) | null,
62
- error: null as unknown,
63
- objectStore: (name: string): IdbStoreLike => store(name),
64
- };
65
- const abort = (): void => {
66
- if (failed) return;
67
- failed = true;
68
- tx.error = new DOMException('The quota has been exceeded.', 'QuotaExceededError');
69
- queueMicrotask(() => tx.onabort?.());
70
- };
71
- const settleLater = (): void => {
72
- queueMicrotask(() => {
73
- if (open > 0 || failed) return;
74
- tx.oncomplete?.();
75
- });
76
- };
77
- const run = <T>(work: () => T): IdbRequestLike<T> => {
78
- const request = pendingRequest<T>();
79
- open += 1;
80
- queueMicrotask(() => {
81
- open -= 1;
82
- request.result = work();
83
- request.onsuccess?.();
84
- settleLater();
85
- });
86
- return request;
87
- };
88
- const store = (name: string): IdbStoreLike => {
89
- const allowed = typeof names === 'string' ? [names] : names;
90
- const table = tables.get(name);
91
- if (!allowed.includes(name) || table === undefined) {
92
- throw new DOMException(`no object store ${name}`, 'NotFoundError');
93
- }
94
- const sorted = (): [string, unknown][] =>
95
- [...table].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
96
- return {
97
- get: (key) => run(() => structuredClone(table.get(key))),
98
- put: (value, key) =>
99
- run(() => {
100
- if (options.quotaExceeded === true) {
101
- abort();
102
- return;
103
- }
104
- table.set(key, structuredClone(value));
105
- }),
106
- delete: (key) => run(() => void table.delete(key)),
107
- getAll: () => run(() => sorted().map(([, value]) => structuredClone(value))),
108
- getAllKeys: () => run(() => sorted().map(([key]) => key)),
109
- };
110
- };
111
- settleLater();
112
- return tx;
113
- },
114
- };
115
- }
116
-
117
- type PendingRequest<T> = IdbRequestLike<T> & { fail(error: unknown): void };
118
-
119
- function pendingRequest<T>(): PendingRequest<T> {
120
- const request: PendingRequest<T> = {
121
- result: undefined as T,
122
- error: null,
123
- onsuccess: null,
124
- onerror: null,
125
- onupgradeneeded: null,
126
- onblocked: null,
127
- fail(error: unknown): void {
128
- request.error = error;
129
- request.onerror?.();
130
- },
131
- };
132
- return request;
133
- }
@@ -1,14 +0,0 @@
1
- // A policy that admits every caller, for the tests that declare a channel whose authz is not
2
- // their subject. Structural, never built with `@ultimat3/policy`: realtime reaches that package
3
- // only through `@ultimat3/query`'s `guard`, and a direct import would be a second authz path.
4
- // An app says the same thing with `allow('public')`, which is what this stands in for.
5
-
6
- import type { QueryPolicy } from '@ultimat3/query';
7
-
8
- export const OPEN_POLICY: QueryPolicy = Object.freeze({
9
- kind: 'allow',
10
- label: 'public',
11
- permissions: [],
12
- children: [],
13
- run: () => ({ allowed: true as const }),
14
- });