syncorejs 0.2.1 → 0.2.3

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 (169) hide show
  1. package/README.md +2 -1
  2. package/dist/_vendor/cli/app.d.mts.map +1 -1
  3. package/dist/_vendor/cli/app.mjs +330 -46
  4. package/dist/_vendor/cli/app.mjs.map +1 -1
  5. package/dist/_vendor/cli/context.mjs +27 -9
  6. package/dist/_vendor/cli/context.mjs.map +1 -1
  7. package/dist/_vendor/cli/dev-session.mjs.map +1 -1
  8. package/dist/_vendor/cli/doctor.mjs +513 -46
  9. package/dist/_vendor/cli/doctor.mjs.map +1 -1
  10. package/dist/_vendor/cli/errors.mjs.map +1 -1
  11. package/dist/_vendor/cli/help.mjs.map +1 -1
  12. package/dist/_vendor/cli/index.mjs +9 -2
  13. package/dist/_vendor/cli/index.mjs.map +1 -1
  14. package/dist/_vendor/cli/messages.mjs +5 -4
  15. package/dist/_vendor/cli/messages.mjs.map +1 -1
  16. package/dist/_vendor/cli/preflight.mjs.map +1 -1
  17. package/dist/_vendor/cli/project.mjs +125 -27
  18. package/dist/_vendor/cli/project.mjs.map +1 -1
  19. package/dist/_vendor/cli/render.mjs +57 -9
  20. package/dist/_vendor/cli/render.mjs.map +1 -1
  21. package/dist/_vendor/cli/targets.mjs +4 -3
  22. package/dist/_vendor/cli/targets.mjs.map +1 -1
  23. package/dist/_vendor/core/cli.d.mts +20 -4
  24. package/dist/_vendor/core/cli.d.mts.map +1 -1
  25. package/dist/_vendor/core/cli.mjs +458 -133
  26. package/dist/_vendor/core/cli.mjs.map +1 -1
  27. package/dist/_vendor/core/devtools-auth.mjs +60 -0
  28. package/dist/_vendor/core/devtools-auth.mjs.map +1 -0
  29. package/dist/_vendor/core/index.d.mts +5 -3
  30. package/dist/_vendor/core/index.mjs +22 -2
  31. package/dist/_vendor/core/index.mjs.map +1 -1
  32. package/dist/_vendor/core/runtime/components.d.mts +111 -0
  33. package/dist/_vendor/core/runtime/components.d.mts.map +1 -0
  34. package/dist/_vendor/core/runtime/components.mjs +186 -0
  35. package/dist/_vendor/core/runtime/components.mjs.map +1 -0
  36. package/dist/_vendor/core/runtime/devtools.d.mts +4 -4
  37. package/dist/_vendor/core/runtime/devtools.d.mts.map +1 -1
  38. package/dist/_vendor/core/runtime/devtools.mjs +178 -60
  39. package/dist/_vendor/core/runtime/devtools.mjs.map +1 -1
  40. package/dist/_vendor/core/runtime/functions.d.mts +398 -16
  41. package/dist/_vendor/core/runtime/functions.d.mts.map +1 -1
  42. package/dist/_vendor/core/runtime/functions.mjs +74 -3
  43. package/dist/_vendor/core/runtime/functions.mjs.map +1 -1
  44. package/dist/_vendor/core/runtime/id.d.mts.map +1 -1
  45. package/dist/_vendor/core/runtime/id.mjs.map +1 -1
  46. package/dist/_vendor/core/runtime/internal/engines/devtoolsEngine.mjs +83 -0
  47. package/dist/_vendor/core/runtime/internal/engines/devtoolsEngine.mjs.map +1 -0
  48. package/dist/_vendor/core/runtime/internal/engines/executionEngine.mjs +720 -0
  49. package/dist/_vendor/core/runtime/internal/engines/executionEngine.mjs.map +1 -0
  50. package/dist/_vendor/core/runtime/internal/engines/reactivityEngine.mjs +234 -0
  51. package/dist/_vendor/core/runtime/internal/engines/reactivityEngine.mjs.map +1 -0
  52. package/dist/_vendor/core/runtime/internal/engines/schedulerEngine.mjs +255 -0
  53. package/dist/_vendor/core/runtime/internal/engines/schedulerEngine.mjs.map +1 -0
  54. package/dist/_vendor/core/runtime/internal/engines/schemaEngine.mjs +200 -0
  55. package/dist/_vendor/core/runtime/internal/engines/schemaEngine.mjs.map +1 -0
  56. package/dist/_vendor/core/runtime/internal/engines/shared.mjs +252 -0
  57. package/dist/_vendor/core/runtime/internal/engines/shared.mjs.map +1 -0
  58. package/dist/_vendor/core/runtime/internal/engines/storageEngine.mjs +145 -0
  59. package/dist/_vendor/core/runtime/internal/engines/storageEngine.mjs.map +1 -0
  60. package/dist/_vendor/core/runtime/internal/runtimeKernel.mjs +221 -0
  61. package/dist/_vendor/core/runtime/internal/runtimeKernel.mjs.map +1 -0
  62. package/dist/_vendor/core/runtime/internal/runtimeStatus.mjs +32 -0
  63. package/dist/_vendor/core/runtime/internal/runtimeStatus.mjs.map +1 -0
  64. package/dist/_vendor/core/runtime/internal/systemMeta.mjs +61 -0
  65. package/dist/_vendor/core/runtime/internal/systemMeta.mjs.map +1 -0
  66. package/dist/_vendor/core/runtime/internal/transactionCoordinator.mjs +41 -0
  67. package/dist/_vendor/core/runtime/internal/transactionCoordinator.mjs.map +1 -0
  68. package/dist/_vendor/core/runtime/runtime.d.mts +1187 -202
  69. package/dist/_vendor/core/runtime/runtime.d.mts.map +1 -1
  70. package/dist/_vendor/core/runtime/runtime.mjs +73 -1365
  71. package/dist/_vendor/core/runtime/runtime.mjs.map +1 -1
  72. package/dist/_vendor/core/transport.d.mts +113 -0
  73. package/dist/_vendor/core/transport.d.mts.map +1 -0
  74. package/dist/_vendor/core/transport.mjs +428 -0
  75. package/dist/_vendor/core/transport.mjs.map +1 -0
  76. package/dist/_vendor/devtools-protocol/index.d.ts +187 -4
  77. package/dist/_vendor/devtools-protocol/index.d.ts.map +1 -1
  78. package/dist/_vendor/devtools-protocol/index.js +25 -9
  79. package/dist/_vendor/devtools-protocol/index.js.map +1 -1
  80. package/dist/_vendor/next/config.d.ts +3 -4
  81. package/dist/_vendor/next/config.d.ts.map +1 -1
  82. package/dist/_vendor/next/config.js +37 -19
  83. package/dist/_vendor/next/config.js.map +1 -1
  84. package/dist/_vendor/next/index.d.ts +109 -29
  85. package/dist/_vendor/next/index.d.ts.map +1 -1
  86. package/dist/_vendor/next/index.js +104 -26
  87. package/dist/_vendor/next/index.js.map +1 -1
  88. package/dist/_vendor/platform-expo/index.d.ts +156 -37
  89. package/dist/_vendor/platform-expo/index.d.ts.map +1 -1
  90. package/dist/_vendor/platform-expo/index.js +80 -12
  91. package/dist/_vendor/platform-expo/index.js.map +1 -1
  92. package/dist/_vendor/platform-expo/react.d.ts.map +1 -1
  93. package/dist/_vendor/platform-expo/react.js +11 -10
  94. package/dist/_vendor/platform-expo/react.js.map +1 -1
  95. package/dist/_vendor/platform-expo/web-sqljs-wasm.js +16 -0
  96. package/dist/_vendor/platform-expo/web-sqljs-wasm.js.map +1 -0
  97. package/dist/_vendor/platform-node/index.d.mts +192 -24
  98. package/dist/_vendor/platform-node/index.d.mts.map +1 -1
  99. package/dist/_vendor/platform-node/index.mjs +236 -97
  100. package/dist/_vendor/platform-node/index.mjs.map +1 -1
  101. package/dist/_vendor/platform-node/ipc-react.d.mts.map +1 -1
  102. package/dist/_vendor/platform-node/ipc-react.mjs +15 -2
  103. package/dist/_vendor/platform-node/ipc-react.mjs.map +1 -1
  104. package/dist/_vendor/platform-node/ipc.d.mts +11 -35
  105. package/dist/_vendor/platform-node/ipc.d.mts.map +1 -1
  106. package/dist/_vendor/platform-node/ipc.mjs +3 -273
  107. package/dist/_vendor/platform-node/ipc.mjs.map +1 -1
  108. package/dist/_vendor/platform-web/external-change.d.ts +43 -1
  109. package/dist/_vendor/platform-web/external-change.d.ts.map +1 -1
  110. package/dist/_vendor/platform-web/external-change.js +32 -1
  111. package/dist/_vendor/platform-web/external-change.js.map +1 -1
  112. package/dist/_vendor/platform-web/index.d.ts +323 -51
  113. package/dist/_vendor/platform-web/index.d.ts.map +1 -1
  114. package/dist/_vendor/platform-web/index.js +233 -30
  115. package/dist/_vendor/platform-web/index.js.map +1 -1
  116. package/dist/_vendor/platform-web/indexeddb.d.ts +12 -0
  117. package/dist/_vendor/platform-web/indexeddb.d.ts.map +1 -1
  118. package/dist/_vendor/platform-web/indexeddb.js +10 -0
  119. package/dist/_vendor/platform-web/indexeddb.js.map +1 -1
  120. package/dist/_vendor/platform-web/opfs.d.ts +13 -0
  121. package/dist/_vendor/platform-web/opfs.d.ts.map +1 -1
  122. package/dist/_vendor/platform-web/opfs.js +12 -0
  123. package/dist/_vendor/platform-web/opfs.js.map +1 -1
  124. package/dist/_vendor/platform-web/persistence.d.ts +54 -0
  125. package/dist/_vendor/platform-web/persistence.d.ts.map +1 -1
  126. package/dist/_vendor/platform-web/persistence.js +15 -0
  127. package/dist/_vendor/platform-web/persistence.js.map +1 -1
  128. package/dist/_vendor/platform-web/react.d.ts +1 -2
  129. package/dist/_vendor/platform-web/react.d.ts.map +1 -1
  130. package/dist/_vendor/platform-web/react.js +27 -13
  131. package/dist/_vendor/platform-web/react.js.map +1 -1
  132. package/dist/_vendor/platform-web/sqljs.js +10 -1
  133. package/dist/_vendor/platform-web/sqljs.js.map +1 -1
  134. package/dist/_vendor/platform-web/web-sqljs-wasm.js +8 -0
  135. package/dist/_vendor/platform-web/web-sqljs-wasm.js.map +1 -0
  136. package/dist/_vendor/platform-web/worker.d.ts +71 -44
  137. package/dist/_vendor/platform-web/worker.d.ts.map +1 -1
  138. package/dist/_vendor/platform-web/worker.js +40 -271
  139. package/dist/_vendor/platform-web/worker.js.map +1 -1
  140. package/dist/_vendor/react/index.d.ts +222 -23
  141. package/dist/_vendor/react/index.d.ts.map +1 -1
  142. package/dist/_vendor/react/index.js +476 -63
  143. package/dist/_vendor/react/index.js.map +1 -1
  144. package/dist/_vendor/schema/definition.d.ts +151 -37
  145. package/dist/_vendor/schema/definition.d.ts.map +1 -1
  146. package/dist/_vendor/schema/definition.js +102 -20
  147. package/dist/_vendor/schema/definition.js.map +1 -1
  148. package/dist/_vendor/schema/index.d.ts +4 -4
  149. package/dist/_vendor/schema/index.js +2 -2
  150. package/dist/_vendor/schema/planner.d.ts +19 -2
  151. package/dist/_vendor/schema/planner.d.ts.map +1 -1
  152. package/dist/_vendor/schema/planner.js +79 -3
  153. package/dist/_vendor/schema/planner.js.map +1 -1
  154. package/dist/_vendor/schema/validators.d.ts +279 -83
  155. package/dist/_vendor/schema/validators.d.ts.map +1 -1
  156. package/dist/_vendor/schema/validators.js +330 -38
  157. package/dist/_vendor/schema/validators.js.map +1 -1
  158. package/dist/_vendor/svelte/index.d.ts +245 -19
  159. package/dist/_vendor/svelte/index.d.ts.map +1 -1
  160. package/dist/_vendor/svelte/index.js +443 -20
  161. package/dist/_vendor/svelte/index.js.map +1 -1
  162. package/dist/browser.d.ts.map +1 -1
  163. package/dist/cli.js +3 -1
  164. package/dist/cli.js.map +1 -1
  165. package/dist/components.d.ts +2 -0
  166. package/dist/components.js +2 -0
  167. package/dist/index.d.ts +3 -2
  168. package/dist/index.js +2 -1
  169. package/package.json +29 -21
@@ -2,16 +2,50 @@ import { createContext, useContext, useEffect, useMemo, useState } from "react";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  //#region src/index.tsx
4
4
  /**
5
- * Pass `"skip"` as the args argument to `useQuery` to suppress the subscription
6
- * entirely and return `undefined` without contacting the runtime.
5
+ * Pass `skip` as the `args` argument to any Syncore React hook to suppress
6
+ * that subscription entirely.
7
+ *
8
+ * Useful when the query arguments depend on state that is not yet available
9
+ * (e.g. a selected item ID) — instead of conditionally calling the hook
10
+ * (which violates the Rules of Hooks), pass `skip` to deactivate it:
11
+ *
12
+ * ```tsx
13
+ * const task = useQuery(api.tasks.get, selectedId ? { id: selectedId } : skip);
14
+ * // task is `undefined` while selectedId is null/undefined
15
+ * ```
16
+ *
17
+ * Skipped queries return `undefined` for `data`, `"skipped"` for `status`,
18
+ * and `false` for `isLoading`.
7
19
  */
8
20
  const skip = "skip";
21
+ const defaultRuntimeStatus = {
22
+ kind: "starting",
23
+ reason: "booting"
24
+ };
9
25
  const SyncoreContext = createContext(null);
10
26
  /**
11
- * Provide a Syncore client to React descendants.
27
+ * Provides a Syncore client to all React descendants via context.
28
+ *
29
+ * Wrap your app (or any subtree that uses Syncore hooks) with
30
+ * `SyncoreProvider`. All `useQuery`, `useMutation`, `useAction`, and
31
+ * `useQueries` calls inside the tree will automatically use the client you
32
+ * supply.
33
+ *
34
+ * ```tsx
35
+ * // For a browser worker setup
36
+ * const client = createBrowserWorkerClient();
37
+ *
38
+ * function App() {
39
+ * return (
40
+ * <SyncoreProvider client={client}>
41
+ * <TaskList />
42
+ * </SyncoreProvider>
43
+ * );
44
+ * }
45
+ * ```
12
46
  *
13
- * Wrap your app with this component to use Syncore hooks like `useQuery` and
14
- * `useMutation`.
47
+ * For Next.js apps use `SyncoreNextProvider` which also handles service worker
48
+ * and worker URL configuration.
15
49
  */
16
50
  function SyncoreProvider({ client, children }) {
17
51
  return /* @__PURE__ */ jsx(SyncoreContext.Provider, {
@@ -20,9 +54,17 @@ function SyncoreProvider({ client, children }) {
20
54
  });
21
55
  }
22
56
  /**
23
- * Read the active Syncore client from React context.
57
+ * Returns the active `SyncoreClient` from the nearest {@link SyncoreProvider}
58
+ * in the React tree.
24
59
  *
25
- * Throws if used outside of {@link SyncoreProvider}.
60
+ * Throws if called outside of a `SyncoreProvider`. Prefer the higher-level
61
+ * hooks (`useQuery`, `useMutation`, etc.) for common operations — use
62
+ * `useSyncore` only when you need direct access to the client object.
63
+ *
64
+ * ```ts
65
+ * const client = useSyncore();
66
+ * const tasks = await client.query(api.tasks.list);
67
+ * ```
26
68
  */
27
69
  function useSyncore() {
28
70
  const client = useContext(SyncoreContext);
@@ -30,15 +72,90 @@ function useSyncore() {
30
72
  return client;
31
73
  }
32
74
  /**
33
- * Load a reactive Syncore query within a React component.
75
+ * Subscribe to the runtime’s lifecycle status.
76
+ *
77
+ * Returns a {@link SyncoreRuntimeStatus} that updates whenever the underlying
78
+ * runtime changes state (e.g. starting, ready, error). Use it to gate your UI
79
+ * on the runtime being ready or to display an error boundary:
80
+ *
81
+ * ```tsx
82
+ * function TaskList() {
83
+ * const status = useSyncoreStatus();
84
+ * if (status.kind === "starting") return <Spinner />;
85
+ * if (status.kind === "error") return <ErrorScreen error={status.error} />;
86
+ * return <Tasks />;
87
+ * }
88
+ * ```
89
+ *
90
+ * Most components do not need this — `useQuery` already incorporates runtime
91
+ * status into the `SyncoreQueryState.runtimeStatus` field.
92
+ */
93
+ function useSyncoreStatus() {
94
+ const client = useSyncore();
95
+ const watch = useMemo(() => client.watchRuntimeStatus(), [client]);
96
+ const [status, setStatus] = useState(() => readRuntimeStatusSnapshot(watch));
97
+ useEffect(() => {
98
+ const sync = () => {
99
+ setStatus(readRuntimeStatusSnapshot(watch));
100
+ };
101
+ sync();
102
+ return watch.onUpdate(sync);
103
+ }, [watch]);
104
+ useEffect(() => () => {
105
+ watch.dispose?.();
106
+ }, [watch]);
107
+ return status;
108
+ }
109
+ /**
110
+ * Subscribe to a reactive Syncore query and return the current data.
111
+ *
112
+ * The component re-renders automatically whenever the query result changes.
113
+ * If the query throws, `useQuery` re-throws the error so a React error
114
+ * boundary can catch it — use {@link useQueryState} if you need to handle
115
+ * errors inline.
116
+ *
117
+ * ```tsx
118
+ * // Basic usage
119
+ * const tasks = useQuery(api.tasks.list);
120
+ *
121
+ * // With arguments
122
+ * const task = useQuery(api.tasks.get, { id: taskId });
123
+ *
124
+ * // Conditionally skip when arguments are not yet available
125
+ * const task = useQuery(api.tasks.get, taskId ? { id: taskId } : skip);
126
+ * ```
34
127
  *
35
- * The hook subscribes automatically and re-renders whenever the local query
36
- * result changes. Pass `"skip"` as the second argument to suppress the
37
- * subscription entirely and return `undefined` without contacting the runtime.
128
+ * @param reference - A typed function reference (from the generated `api` object).
129
+ * @param args - The query’s arguments, or `skip` to suppress the subscription.
130
+ * @returns The current query result, or `undefined` while loading.
38
131
  */
39
132
  function useQuery(reference, ...args) {
133
+ const state = useQueryState(reference, ...args);
134
+ if (state.error) throw state.error;
135
+ return state.data;
136
+ }
137
+ /**
138
+ * Subscribe to a reactive Syncore query and return the full
139
+ * {@link SyncoreQueryState} including loading, error, and runtime status.
140
+ *
141
+ * Use this instead of {@link useQuery} when you need to:
142
+ * - Differentiate between `undefined` data and an error.
143
+ * - React to `isLoading` / `isError` without relying on error boundaries.
144
+ * - Inspect `runtimeStatus` for the underlying runtime’s health.
145
+ *
146
+ * ```tsx
147
+ * const { data, isLoading, isError, error } = useQueryState(api.tasks.list);
148
+ *
149
+ * if (isLoading) return <Spinner />;
150
+ * if (isError) return <ErrorBanner message={error.message} />;
151
+ * return <TaskList tasks={data} />;
152
+ * ```
153
+ */
154
+ function useQueryState(reference, ...args) {
40
155
  const isSkipped = args[0] === skip;
41
- const watch = useManagedQueryWatch(useSyncore(), reference, isSkipped ? void 0 : normalizeOptionalArgs(args), isSkipped);
156
+ const client = useSyncore();
157
+ const runtimeStatus = useSyncoreStatus();
158
+ const watch = useManagedQueryWatch(client, reference, isSkipped ? void 0 : normalizeOptionalArgs(args), isSkipped);
42
159
  const [snapshot, setSnapshot] = useState(() => isSkipped ? noOpSnapshot : readWatchSnapshot(watch));
43
160
  useEffect(() => {
44
161
  if (isSkipped) {
@@ -51,78 +168,277 @@ function useQuery(reference, ...args) {
51
168
  sync();
52
169
  return watch.onUpdate(sync);
53
170
  }, [watch, isSkipped]);
54
- if (snapshot.error) throw snapshot.error;
55
- return snapshot.result;
171
+ return toQueryState(snapshot, runtimeStatus, isSkipped);
56
172
  }
57
- const noOpSnapshot = {
58
- result: void 0,
59
- error: void 0
60
- };
61
- const noOpWatch = {
62
- onUpdate: () => () => {},
63
- localQueryResult: () => void 0,
64
- localQueryError: () => void 0
65
- };
66
173
  /**
67
- * Construct a stable function that executes a Syncore mutation.
174
+ * Returns a stable callback for executing a Syncore mutation.
175
+ *
176
+ * The returned function is type-safe: its parameter types are inferred from
177
+ * the mutation definition and remain stable across re-renders (no need to
178
+ * wrap in `useCallback`).
179
+ *
180
+ * ```tsx
181
+ * const createTask = useMutation(api.tasks.create);
182
+ *
183
+ * return (
184
+ * <button onClick={() => createTask({ title: "New task" })}>
185
+ * Add task
186
+ * </button>
187
+ * );
188
+ * ```
189
+ *
190
+ * @param reference - A typed mutation reference from the generated `api` object.
191
+ * @returns A function that, when called, executes the mutation and returns a
192
+ * promise that resolves to the mutation’s return value.
68
193
  */
69
194
  function useMutation(reference) {
70
195
  const client = useSyncore();
71
196
  return (...args) => client.mutation(reference, normalizeOptionalArgs(args));
72
197
  }
73
198
  /**
74
- * Construct a stable function that executes a Syncore action.
199
+ * Returns a stable callback for executing a Syncore action.
200
+ *
201
+ * Identical to {@link useMutation} but for actions. Use this when the work you
202
+ * need to do cannot run inside a transaction (external API calls, long-running
203
+ * tasks, etc.).
204
+ *
205
+ * ```tsx
206
+ * const importTasks = useAction(api.tasks.importFromCsv);
207
+ *
208
+ * return (
209
+ * <button onClick={() => importTasks({ url: csvUrl })}>
210
+ * Import
211
+ * </button>
212
+ * );
213
+ * ```
214
+ *
215
+ * @param reference - A typed action reference from the generated `api` object.
216
+ * @returns A function that, when called, executes the action and returns a
217
+ * promise that resolves to the action’s return value.
75
218
  */
76
219
  function useAction(reference) {
77
220
  const client = useSyncore();
78
221
  return (...args) => client.action(reference, normalizeOptionalArgs(args));
79
222
  }
80
223
  /**
81
- * Load several Syncore queries at once using explicit keys.
224
+ * Subscribe to multiple Syncore queries simultaneously and receive per-entry
225
+ * state objects in a single hook call.
226
+ *
227
+ * More efficient than calling `useQuery` in a loop when the set of queries is
228
+ * known at component render time. The hook maintains only one subscription per
229
+ * unique `(reference, args)` combination even if entries are duplicated.
230
+ *
231
+ * ```tsx
232
+ * const { header, sidebar } = useQueries({
233
+ * header: { query: api.layout.header },
234
+ * sidebar: { query: api.layout.sidebar, args: { userId } },
235
+ * });
236
+ *
237
+ * if (header.isLoading || sidebar.isLoading) return <Spinner />;
238
+ * ```
239
+ *
240
+ * @param entries - A record of named query requests. Each entry can include
241
+ * `args: skip` to suppress that specific subscription.
242
+ * @returns A record with the same keys, each holding a {@link SyncoreQueryState}.
82
243
  */
83
244
  function useQueries(entries) {
84
245
  const client = useSyncore();
85
- const entriesKey = stableStringify(entries.map((entry) => ({
86
- key: entry.key,
87
- referenceName: entry.reference.name,
88
- args: entry.args ?? {}
246
+ const runtimeStatus = useSyncoreStatus();
247
+ const entriesKey = stableStringify(Object.entries(entries).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => ({
248
+ key,
249
+ referenceName: entry.query.name,
250
+ skipped: entry.args === skip,
251
+ args: entry.args === "skip" ? {} : normalizeOptionalArgs([entry.args ?? {}])
89
252
  })));
90
253
  const normalizedEntries = useMemo(() => JSON.parse(entriesKey), [entriesKey]);
91
- const watches = useMemo(() => normalizedEntries.map((entry) => ({
92
- key: entry.key,
93
- watch: client.watchQuery({
94
- kind: "query",
95
- name: entry.referenceName
96
- }, entry.args)
97
- })), [client, normalizedEntries]);
98
- const [snapshot, setSnapshot] = useState(() => readQueriesSnapshot(watches));
99
- useEffect(() => () => {
100
- for (const entry of watches) entry.watch.dispose?.();
101
- }, [watches]);
254
+ const [observer] = useState(() => new ReactQueriesObserver(client));
255
+ const [, setVersion] = useState(0);
256
+ if (observer.client !== client) observer.replaceClient(client);
257
+ useEffect(() => () => observer.destroy(), [observer]);
102
258
  useEffect(() => {
103
- const sync = () => {
104
- setSnapshot(readQueriesSnapshot(watches));
259
+ observer.setEntries(normalizedEntries);
260
+ setVersion((value) => value + 1);
261
+ return observer.subscribe(() => {
262
+ setVersion((value) => value + 1);
263
+ });
264
+ }, [normalizedEntries, observer]);
265
+ const snapshot = observer.getSnapshot(normalizedEntries);
266
+ return useMemo(() => {
267
+ return Object.fromEntries(normalizedEntries.map((entry) => [entry.key, toQueryState(snapshot[entry.key] ?? noOpSnapshot, runtimeStatus, entry.skipped)]));
268
+ }, [
269
+ normalizedEntries,
270
+ runtimeStatus,
271
+ snapshot
272
+ ]);
273
+ }
274
+ /**
275
+ * Subscribe to a paginated Syncore query, incrementally loading more pages.
276
+ *
277
+ * The query must accept a `paginationOpts` argument and return a
278
+ * `PaginationResult`. The hook manages cursors automatically — call the
279
+ * returned `loadMore` function to append the next page to the results.
280
+ *
281
+ * ```tsx
282
+ * const { results, status, loadMore, hasMore } = usePaginatedQuery(
283
+ * api.tasks.list,
284
+ * { projectId },
285
+ * { initialNumItems: 20 },
286
+ * );
287
+ *
288
+ * return (
289
+ * <>
290
+ * {results.map((t) => <TaskRow key={t._id} task={t} />)}
291
+ * {hasMore && (
292
+ * <button
293
+ * onClick={() => loadMore(20)}
294
+ * disabled={status === "loadingMore"}
295
+ * >
296
+ * Load more
297
+ * </button>
298
+ * )}
299
+ * </>
300
+ * );
301
+ * ```
302
+ *
303
+ * Pass `skip` as `args` to suppress the subscription until arguments are
304
+ * ready.
305
+ *
306
+ * @param reference - A typed query reference whose handler calls
307
+ * `ctx.db.query(…).paginate(paginationOpts)`.
308
+ * @param args - Arguments for the query (excluding
309
+ * `paginationOpts`, which is managed internally), or `skip`.
310
+ * @param options.initialNumItems - Number of items to load on the first page.
311
+ * @returns A {@link UsePaginatedQueryResult} with the accumulated results and
312
+ * a `loadMore` callback.
313
+ */
314
+ function usePaginatedQuery(reference, args, options) {
315
+ if (typeof options.initialNumItems !== "number" || options.initialNumItems <= 0) throw new Error(`options.initialNumItems must be a positive number. Received ${String(options.initialNumItems)}.`);
316
+ const runtimeStatus = useSyncoreStatus();
317
+ const isSkipped = args === skip;
318
+ const normalizedArgs = isSkipped ? {} : args ?? {};
319
+ const requestKey = stableStringify({
320
+ referenceName: reference.name,
321
+ args: normalizedArgs,
322
+ initialNumItems: options.initialNumItems,
323
+ skipped: isSkipped
324
+ });
325
+ const createInitialState = useMemo(() => () => ({
326
+ requestKey,
327
+ nextPageKey: 1,
328
+ pages: isSkipped ? [] : [{
329
+ key: "0",
330
+ cursor: null,
331
+ numItems: options.initialNumItems
332
+ }]
333
+ }), [
334
+ isSkipped,
335
+ options.initialNumItems,
336
+ requestKey
337
+ ]);
338
+ const [state, setState] = useState(createInitialState);
339
+ let currentState = state;
340
+ if (currentState.requestKey !== requestKey) {
341
+ currentState = createInitialState();
342
+ setState(currentState);
343
+ }
344
+ const pageStates = useQueries(useMemo(() => {
345
+ const requests = {};
346
+ for (const page of currentState.pages) requests[page.key] = {
347
+ query: reference,
348
+ args: {
349
+ ...normalizedArgs,
350
+ paginationOpts: {
351
+ cursor: page.cursor,
352
+ numItems: page.numItems
353
+ }
354
+ }
105
355
  };
106
- sync();
107
- const cleanups = watches.map((entry) => entry.watch.onUpdate(sync));
108
- return () => {
109
- for (const cleanup of cleanups) cleanup();
356
+ return requests;
357
+ }, [
358
+ currentState.pages,
359
+ normalizedArgs,
360
+ reference
361
+ ]));
362
+ const derived = useMemo(() => {
363
+ const pages = [];
364
+ let error;
365
+ for (const page of currentState.pages) {
366
+ const pageState = pageStates[page.key];
367
+ if (!pageState || pageState.status === "loading") break;
368
+ if (pageState.status === "error") {
369
+ error = pageState.error;
370
+ break;
371
+ }
372
+ if (pageState.data) pages.push(pageState.data);
373
+ }
374
+ const results = pages.flatMap((page) => page.page);
375
+ const lastLoadedPage = pages.at(-1);
376
+ const lastRequestedKey = currentState.pages.at(-1)?.key;
377
+ const lastRequestedState = lastRequestedKey ? pageStates[lastRequestedKey] : void 0;
378
+ const isLoading = !isSkipped && pages.length === 0 && !error;
379
+ const isLoadingMore = currentState.pages.length > pages.length || !!lastRequestedState && lastRequestedState.status === "loading" && pages.length > 0;
380
+ const hasMore = !!lastLoadedPage && !lastLoadedPage.isDone;
381
+ const status = error ? "error" : isSkipped ? "ready" : isLoading ? "loading" : isLoadingMore ? "loadingMore" : hasMore ? "ready" : "exhausted";
382
+ return {
383
+ pages,
384
+ results,
385
+ error,
386
+ isLoading,
387
+ isLoadingMore,
388
+ hasMore,
389
+ cursor: lastLoadedPage?.cursor ?? null,
390
+ status
110
391
  };
111
- }, [watches]);
112
- return snapshot;
392
+ }, [
393
+ currentState.pages,
394
+ isSkipped,
395
+ pageStates
396
+ ]);
397
+ return {
398
+ ...derived,
399
+ runtimeStatus,
400
+ loadMore(numItems = options.initialNumItems) {
401
+ if (isSkipped || derived.error || derived.isLoadingMore || !derived.hasMore || !derived.cursor) return;
402
+ setState((previous) => ({
403
+ ...previous,
404
+ nextPageKey: previous.nextPageKey + 1,
405
+ pages: [...previous.pages, {
406
+ key: String(previous.nextPageKey),
407
+ cursor: derived.cursor,
408
+ numItems
409
+ }]
410
+ }));
411
+ }
412
+ };
113
413
  }
114
- function useManagedQueryWatch(client, reference, args, isSkipped) {
115
- const argsKey = isSkipped ? "skip" : stableStringify(args ?? {});
116
- const normalizedArgs = useMemo(() => isSkipped ? void 0 : JSON.parse(argsKey), [argsKey, isSkipped]);
117
- const watch = useMemo(() => isSkipped ? noOpWatch : client.watchQuery(reference, normalizedArgs), [
414
+ const noOpSnapshot = {
415
+ data: void 0,
416
+ error: void 0
417
+ };
418
+ const noOpWatch = {
419
+ onUpdate: () => () => void 0,
420
+ localQueryResult: () => void 0,
421
+ localQueryError: () => void 0
422
+ };
423
+ function useManagedQueryWatch(client, reference, args, isSkipped = false) {
424
+ const argsKey = isSkipped ? skip : stableStringify(args ?? {});
425
+ const [watch, setWatch] = useState(() => noOpWatch);
426
+ useEffect(() => {
427
+ if (isSkipped) {
428
+ setWatch(noOpWatch);
429
+ return;
430
+ }
431
+ const nextWatch = client.watchQuery(reference, JSON.parse(argsKey));
432
+ setWatch(nextWatch);
433
+ return () => {
434
+ nextWatch.dispose?.();
435
+ };
436
+ }, [
437
+ argsKey,
118
438
  client,
119
- normalizedArgs,
120
- reference,
121
- isSkipped
439
+ isSkipped,
440
+ reference
122
441
  ]);
123
- useEffect(() => () => {
124
- if (!isSkipped) watch.dispose?.();
125
- }, [watch, isSkipped]);
126
442
  return watch;
127
443
  }
128
444
  function normalizeOptionalArgs(args) {
@@ -130,12 +446,36 @@ function normalizeOptionalArgs(args) {
130
446
  }
131
447
  function readWatchSnapshot(watch) {
132
448
  return {
133
- result: watch.localQueryResult(),
449
+ data: watch.localQueryResult(),
134
450
  error: watch.localQueryError()
135
451
  };
136
452
  }
137
- function readQueriesSnapshot(watches) {
138
- return Object.fromEntries(watches.map((entry) => [entry.key, entry.watch.localQueryResult()]));
453
+ function readQueriesSnapshot(records) {
454
+ return Object.fromEntries(records.map((entry) => [entry.key, entry.snapshot]));
455
+ }
456
+ function readRuntimeStatusSnapshot(watch) {
457
+ return watch.localQueryResult() ?? defaultRuntimeStatus;
458
+ }
459
+ function toQueryState(snapshot, runtimeStatus, isSkipped) {
460
+ if (isSkipped) return {
461
+ data: void 0,
462
+ error: void 0,
463
+ status: "skipped",
464
+ runtimeStatus,
465
+ isLoading: false,
466
+ isError: false,
467
+ isReady: false
468
+ };
469
+ const status = snapshot.error !== void 0 ? "error" : snapshot.data === void 0 ? "loading" : "success";
470
+ return {
471
+ data: snapshot.data,
472
+ error: snapshot.error,
473
+ status,
474
+ runtimeStatus,
475
+ isLoading: status === "loading",
476
+ isError: status === "error",
477
+ isReady: status === "success"
478
+ };
139
479
  }
140
480
  function stableStringify(value) {
141
481
  return JSON.stringify(sortValue(value));
@@ -145,7 +485,80 @@ function sortValue(value) {
145
485
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortValue(nested)]));
146
486
  return value;
147
487
  }
488
+ var ReactQueriesObserver = class {
489
+ client;
490
+ listeners = /* @__PURE__ */ new Set();
491
+ records = /* @__PURE__ */ new Map();
492
+ constructor(client) {
493
+ this.client = client;
494
+ }
495
+ replaceClient(client) {
496
+ this.destroy();
497
+ this.client = client;
498
+ }
499
+ setEntries(entries) {
500
+ const activeKeys = new Set(entries.map((entry) => entry.key));
501
+ for (const entry of entries) {
502
+ const requestKey = `${entry.referenceName}:${stableStringify(entry.args)}:${String(entry.skipped)}`;
503
+ const current = this.records.get(entry.key);
504
+ if (current?.requestKey === requestKey) continue;
505
+ current?.unsubscribe();
506
+ current?.watch?.dispose?.();
507
+ if (entry.skipped) {
508
+ this.records.set(entry.key, {
509
+ requestKey,
510
+ snapshot: noOpSnapshot,
511
+ unsubscribe: () => void 0
512
+ });
513
+ continue;
514
+ }
515
+ const watch = this.client.watchQuery({
516
+ kind: "query",
517
+ name: entry.referenceName
518
+ }, entry.args);
519
+ const record = {
520
+ requestKey,
521
+ snapshot: readWatchSnapshot(watch),
522
+ unsubscribe: () => void 0,
523
+ watch
524
+ };
525
+ record.unsubscribe = watch.onUpdate(() => {
526
+ record.snapshot = readWatchSnapshot(watch);
527
+ this.notify();
528
+ });
529
+ this.records.set(entry.key, record);
530
+ }
531
+ for (const [key, record] of this.records.entries()) {
532
+ if (activeKeys.has(key)) continue;
533
+ record.unsubscribe();
534
+ record.watch?.dispose?.();
535
+ this.records.delete(key);
536
+ }
537
+ }
538
+ getSnapshot(entries) {
539
+ return readQueriesSnapshot(entries.map((entry) => ({
540
+ key: entry.key,
541
+ snapshot: this.records.get(entry.key)?.snapshot ?? noOpSnapshot
542
+ })));
543
+ }
544
+ subscribe(listener) {
545
+ this.listeners.add(listener);
546
+ return () => {
547
+ this.listeners.delete(listener);
548
+ };
549
+ }
550
+ destroy() {
551
+ for (const record of this.records.values()) {
552
+ record.unsubscribe();
553
+ record.watch?.dispose?.();
554
+ }
555
+ this.records.clear();
556
+ }
557
+ notify() {
558
+ for (const listener of this.listeners) listener();
559
+ }
560
+ };
148
561
  //#endregion
149
- export { SyncoreProvider, skip, useAction, useMutation, useQueries, useQuery, useSyncore };
562
+ export { SyncoreProvider, skip, useAction, useMutation, usePaginatedQuery, useQueries, useQuery, useQueryState, useSyncore, useSyncoreStatus };
150
563
 
151
564
  //# sourceMappingURL=index.js.map