@kontsedal/olas-core 0.0.3 → 0.0.5

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.
@@ -118,6 +118,36 @@ export type GcEvent = {
118
118
  kind: 'data' | 'infinite'
119
119
  }
120
120
 
121
+ /**
122
+ * Emitted when a persistable mutation (`spec.persist === true`) starts
123
+ * executing — before the user's `mutate` is invoked. Plugins use this to
124
+ * persist the variables to durable storage; if the page reloads mid-run,
125
+ * the queue replays from these entries.
126
+ *
127
+ * `runId` is unique per execution (a single `mutation.run(...)` call OR a
128
+ * replay attempt). `attempt` counts retry passes within a single runId.
129
+ */
130
+ export type MutationEnqueueEvent = {
131
+ mutationId: string
132
+ runId: string
133
+ variables: unknown
134
+ attempt: number
135
+ }
136
+
137
+ /**
138
+ * Emitted after a persistable mutation settles. Plugins use this to drop
139
+ * the run from the durable queue (on `'success'` or `'error'` after retries
140
+ * exhaust), or to leave it pending (on `'cancelled'` — e.g. parent dispose
141
+ * mid-flight).
142
+ */
143
+ export type MutationSettleEvent = {
144
+ mutationId: string
145
+ runId: string
146
+ outcome: 'success' | 'error' | 'cancelled'
147
+ /** Only present on `'error'` — the final thrown value after retries. */
148
+ error?: unknown
149
+ }
150
+
121
151
  /**
122
152
  * Plugin contract. Every hook is optional. Hooks are wrapped in try/catch
123
153
  * by `QueryClient`; thrown exceptions are routed through the root's
@@ -126,12 +156,24 @@ export type GcEvent = {
126
156
  export type QueryClientPlugin = {
127
157
  /**
128
158
  * Called once after the `QueryClient` is constructed. Use it to wire up
129
- * transport listeners and capture the `QueryClientPluginApi`.
159
+ * transport listeners and capture the `QueryClientPluginApi`. SPEC §13.2.
160
+ *
161
+ * Persistable-mutation replay typically happens HERE — module-scope
162
+ * `defineMutation(...)` calls have already registered their handlers by
163
+ * the time `createRoot(...)` runs, so `init` can walk durable storage
164
+ * and re-invoke registered mutates for any pending entries.
130
165
  */
131
166
  init?(api: QueryClientPluginApi): void
132
167
  onSetData?(event: SetDataEvent): void
133
168
  onInvalidate?(event: InvalidateEvent): void
134
169
  onGc?(event: GcEvent): void
170
+ /**
171
+ * Fired when a persistable mutation (`spec.persist === true`) starts
172
+ * executing. SPEC §13.3.
173
+ */
174
+ onMutationEnqueue?(event: MutationEnqueueEvent): void
175
+ /** Fired after a persistable mutation settles. SPEC §13.3. */
176
+ onMutationSettle?(event: MutationSettleEvent): void
135
177
  /** Called from `QueryClient.dispose`. Tear down transports / listeners here. */
136
178
  dispose?(): void
137
179
  }
@@ -183,3 +225,51 @@ export function lookupRegisteredQuery(queryId: string): RegisteredQuery | undefi
183
225
  export function _unregisterQueryById(queryId: string): void {
184
226
  queryRegistry.delete(queryId)
185
227
  }
228
+
229
+ // ────────────────────────────────────────────────────────────────────────────
230
+ // Mutation registry — parallel to query registry. Persistable mutations
231
+ // (`spec.persist === true`) register themselves at module-import time via
232
+ // `defineMutation(...)` so the mutation-queue plugin can replay pending
233
+ // runs at `init` time, BEFORE controllers reconstruct.
234
+ // ────────────────────────────────────────────────────────────────────────────
235
+
236
+ /**
237
+ * Shape stored in the `mutationId → handler` registry. Only the `mutate`
238
+ * function and the id are needed for replay — lifecycle hooks like
239
+ * `onSuccess` / `onError` are per-controller and can't be safely replayed
240
+ * across page reloads (the controller doesn't exist yet).
241
+ */
242
+ export type RegisteredMutation = {
243
+ readonly mutationId: string
244
+ /**
245
+ * Replay-safe `mutate`. Matches `MutationSpec.mutate` 1:1 — receives the
246
+ * variables and an `AbortSignal`. The mutation-queue plugin invokes this
247
+ * directly on replay, so the implementation MUST NOT close over
248
+ * controller-instance state. Module-level deps (a shared `api` client,
249
+ * etc.) are fine.
250
+ */
251
+ readonly mutate: (vars: unknown, signal: AbortSignal) => Promise<unknown>
252
+ }
253
+
254
+ const mutationRegistry = new Map<string, RegisteredMutation>()
255
+
256
+ /** Register a mutation by its `mutationId`. Internal — called from `defineMutation`. */
257
+ export function registerMutationById(mutationId: string, entry: RegisteredMutation): void {
258
+ mutationRegistry.set(mutationId, entry)
259
+ }
260
+
261
+ /**
262
+ * Look up a registered mutation by id. Returns `undefined` when no
263
+ * mutation with that id has been defined — typical when a queue entry
264
+ * references a mutation whose module hasn't been imported (e.g. a
265
+ * code-split route boundary). The plugin should leave such entries in
266
+ * place and retry once the module loads.
267
+ */
268
+ export function lookupRegisteredMutation(mutationId: string): RegisteredMutation | undefined {
269
+ return mutationRegistry.get(mutationId)
270
+ }
271
+
272
+ /** Test-only — drop a registered mutation. Not exported from the package. */
273
+ export function _unregisterMutationById(mutationId: string): void {
274
+ mutationRegistry.delete(mutationId)
275
+ }
@@ -32,6 +32,13 @@ export type AsyncState<T> = {
32
32
  refetch: () => Promise<T>
33
33
  reset: () => void
34
34
  firstValue: () => Promise<T>
35
+ /**
36
+ * Alias of `firstValue()` — clearer name for Suspense / `React.use(...)`
37
+ * use cases. Resolves with `data` on first success (short-circuits if
38
+ * already settled), rejects with `error` on the first failure. Use this
39
+ * to suspend a React tree until the query lands its first value.
40
+ */
41
+ promise: () => Promise<T>
35
42
  }
36
43
 
37
44
  /**
package/src/query/use.ts CHANGED
@@ -98,6 +98,9 @@ class SubscriptionImpl<T, U = T> implements QuerySubscription<U> {
98
98
  return cur.entry.firstValue().then((v) => this.project(v))
99
99
  }
100
100
 
101
+ // Alias surfaced on `AsyncState` for Suspense / React 19 `use(...)`.
102
+ promise = (): Promise<U> => this.firstValue()
103
+
101
104
  private project(v: T): U {
102
105
  return this.select === undefined ? (v as unknown as U) : this.select(v)
103
106
  }
@@ -325,6 +328,9 @@ class InfiniteSubscriptionImpl<TPage, TItem> implements InfiniteQuerySubscriptio
325
328
  return cur.entry.firstValue()
326
329
  }
327
330
 
331
+ // Alias of firstValue() for Suspense / React 19 `use(...)` ergonomics.
332
+ promise = (): Promise<TPage[]> => this.firstValue()
333
+
328
334
  fetchNextPage = (): Promise<void> => {
329
335
  const cur = this.current$.peek()
330
336
  if (!cur) return Promise.resolve()
package/src/testing.ts CHANGED
@@ -111,6 +111,7 @@ export function fakeAsyncState<T>(
111
111
  refetch: () => Promise<T>
112
112
  reset: () => void
113
113
  firstValue: () => Promise<T>
114
+ promise: () => Promise<T>
114
115
  }>,
115
116
  ): AsyncState<T> {
116
117
  const data$: ReadSignal<T | undefined> = signal(overrides?.data)
@@ -127,6 +128,7 @@ export function fakeAsyncState<T>(
127
128
  const refetch = overrides?.refetch ?? (async () => data$.peek() as T)
128
129
  const reset = overrides?.reset ?? (() => {})
129
130
  const firstValue = overrides?.firstValue ?? (async () => data$.peek() as T)
131
+ const promise = overrides?.promise ?? firstValue
130
132
 
131
133
  return {
132
134
  data: data$,
@@ -140,5 +142,6 @@ export function fakeAsyncState<T>(
140
142
  refetch,
141
143
  reset,
142
144
  firstValue,
145
+ promise,
143
146
  }
144
147
  }