@confect/foldkit 10.0.0-next.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Command.ts ADDED
@@ -0,0 +1,452 @@
1
+ import type * as Ref from "@confect/core/Ref";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Match from "effect/Match";
4
+ import type * as Schema from "effect/Schema";
5
+ import * as FoldkitCommand from "foldkit/command";
6
+ import * as Client from "./Client";
7
+
8
+ /**
9
+ * Everything a Command against `Ref_` can fail with before it is folded into
10
+ * an `onError` Message: the ref's typed error (if it declares an `error`
11
+ * schema), a transport-level `WebSocketClientError`, or a `SchemaError` from
12
+ * encoding args or decoding returns.
13
+ */
14
+ export type Error<Ref_ extends Ref.AnyConfect> =
15
+ | Ref.Error<Ref_>
16
+ | Client.WebSocketClientError
17
+ | Schema.SchemaError;
18
+
19
+ /**
20
+ * Maps a Confect function's outcome into the app's Messages. Every failure —
21
+ * the ref's typed error, a transport error, or a codec error — arrives via
22
+ * `onError`, so the resulting Command's error channel is `never`, as Foldkit
23
+ * requires. Follow Foldkit's naming convention: past-tense facts like
24
+ * `SucceededSaveNote` / `FailedSaveNote`.
25
+ */
26
+ export interface Handlers<
27
+ Ref_ extends Ref.AnyConfect,
28
+ SuccessMessage,
29
+ ErrorMessage,
30
+ > {
31
+ readonly onSuccess: (returns: Ref.Returns<Ref_>) => SuccessMessage;
32
+ readonly onError: (error: Error<Ref_>) => ErrorMessage;
33
+ }
34
+
35
+ /**
36
+ * The Command instance a definition call constructs.
37
+ */
38
+ type Instance<
39
+ Name extends string,
40
+ Ref_ extends Ref.AnyConfect,
41
+ Message,
42
+ > = Readonly<{
43
+ name: Name;
44
+ args: Ref.Args<Ref_>;
45
+ effect: Effect.Effect<Message, never, Client.Client>;
46
+ }>;
47
+
48
+ type SchemaArgs<Ref_ extends Ref.AnyConfect> = Schema.Schema.Type<
49
+ Schema.Struct<Ref.ArgsSchema<Ref_>["fields"]>
50
+ >;
51
+
52
+ /**
53
+ * A Foldkit Command definition whose args are the ref's args: our
54
+ * ref-derived call signature (args optional when the ref declares none)
55
+ * intersected with Foldkit's own `CommandDefinitionWithArgs`, which supplies
56
+ * the `CommandDefinitionTypeId` brand and makes the definition assignable
57
+ * wherever Foldkit accepts one — including Story/Scene `Command.resolve`
58
+ * and `expectExact` matchers. Call it from `update` to construct a Command
59
+ * instance; nothing runs until the Foldkit runtime executes it.
60
+ */
61
+ export type Definition<
62
+ Name extends string,
63
+ Ref_ extends Ref.AnyConfect,
64
+ Message,
65
+ > = ((...args: Ref.OptionalArgs<Ref_>) => Instance<Name, Ref_, Message>) &
66
+ FoldkitCommand.CommandDefinitionWithArgs<
67
+ Name,
68
+ Ref.ArgsSchema<Ref_>["fields"],
69
+ Effect.Effect<Message, never, Client.Client>
70
+ >;
71
+
72
+ /**
73
+ * Makes a factory-built Command interruptible — Foldkit's `InterruptOption`
74
+ * applied to the ref's args. `true` keys every invocation by the Command
75
+ * name — right when at most one invocation is meaningfully in flight; a
76
+ * `KeyedInterrupt` derives the key part from the ref's args, so concurrent
77
+ * invocations can be targeted independently.
78
+ *
79
+ * Interrupting stops the client-side Effect and guarantees the invocation's
80
+ * result Messages never dispatch — it does not cancel the Convex function on
81
+ * the server, which runs to completion once the call is on the wire.
82
+ */
83
+ export type InterruptOption<
84
+ Ref_ extends Ref.AnyConfect,
85
+ KeyField extends keyof SchemaArgs<Ref_> & string = keyof SchemaArgs<Ref_> &
86
+ string,
87
+ > = FoldkitCommand.InterruptOption<SchemaArgs<Ref_>, KeyField>;
88
+
89
+ /**
90
+ * The keyed arm of `InterruptOption`: `keyFields` declares the args the
91
+ * `Interrupt` constructor requires; `toKey` derives the key part from them.
92
+ */
93
+ export type KeyedInterrupt<
94
+ Ref_ extends Ref.AnyConfect,
95
+ KeyField extends keyof SchemaArgs<Ref_> & string,
96
+ > = Exclude<InterruptOption<Ref_, KeyField>, true>;
97
+
98
+ /**
99
+ * An interruptible Command definition whose key is the Command name — the
100
+ * ref-derived call signature intersected with Foldkit's
101
+ * `Interruptible.DefinitionWithArgsNameKeyed`, which supplies the brand and
102
+ * the `Interrupt` constructor.
103
+ */
104
+ export type InterruptibleDefinition<
105
+ Name extends string,
106
+ Ref_ extends Ref.AnyConfect,
107
+ Message,
108
+ > = ((
109
+ ...args: Ref.OptionalArgs<Ref_>
110
+ ) => Instance<Name, Ref_, Message> & Readonly<{ key: string }>) &
111
+ FoldkitCommand.Interruptible.DefinitionWithArgsNameKeyed<
112
+ Name,
113
+ Ref.ArgsSchema<Ref_>["fields"],
114
+ Effect.Effect<Message, never, Client.Client>
115
+ >;
116
+
117
+ /**
118
+ * An interruptible Command definition whose key is derived from the ref's
119
+ * args — the ref-derived call signature intersected with Foldkit's
120
+ * `Interruptible.DefinitionWithArgs`, which supplies the brand and the
121
+ * key-args-taking `Interrupt` constructor.
122
+ */
123
+ export type KeyedInterruptibleDefinition<
124
+ Name extends string,
125
+ Ref_ extends Ref.AnyConfect,
126
+ KeyField extends keyof SchemaArgs<Ref_> & string,
127
+ Message,
128
+ > = ((
129
+ ...args: Ref.OptionalArgs<Ref_>
130
+ ) => Instance<Name, Ref_, Message> & Readonly<{ key: string }>) &
131
+ FoldkitCommand.Interruptible.DefinitionWithArgs<
132
+ Name,
133
+ Ref.ArgsSchema<Ref_>["fields"],
134
+ Pick<SchemaArgs<Ref_>, KeyField>,
135
+ Effect.Effect<Message, never, Client.Client>
136
+ >;
137
+
138
+ const run = <Ref_ extends Ref.AnyConfect, SuccessMessage, ErrorMessage>(
139
+ handlers: Handlers<Ref_, SuccessMessage, ErrorMessage>,
140
+ invoke: (
141
+ client: Client.Client,
142
+ ) => Effect.Effect<Ref.Returns<Ref_>, Error<Ref_>>,
143
+ ): Effect.Effect<SuccessMessage | ErrorMessage, never, Client.Client> =>
144
+ Effect.flatMap(Client.Client, invoke).pipe(
145
+ Effect.match({
146
+ onSuccess: handlers.onSuccess,
147
+ onFailure: handlers.onError,
148
+ }),
149
+ );
150
+
151
+ /**
152
+ * An execute body for a hand-written `Command.define` — every failure already
153
+ * folded into a Message. Reach for these when the factory below doesn't fit:
154
+ * a custom args schema, `interrupt`, or a Command that makes several calls.
155
+ */
156
+ export const queryEffect =
157
+ <Query extends Ref.AnyConfectPublicQuery, SuccessMessage, ErrorMessage>(
158
+ ref: Query,
159
+ handlers: Handlers<Query, SuccessMessage, ErrorMessage>,
160
+ ) =>
161
+ (
162
+ ...args: Ref.OptionalArgs<Query>
163
+ ): Effect.Effect<SuccessMessage | ErrorMessage, never, Client.Client> =>
164
+ run(handlers, (client) => client.query(ref, ...args));
165
+
166
+ /**
167
+ * An execute body for a hand-written `Command.define` — every failure already
168
+ * folded into a Message. See `queryEffect`.
169
+ */
170
+ export const mutationEffect =
171
+ <Mutation extends Ref.AnyConfectPublicMutation, SuccessMessage, ErrorMessage>(
172
+ ref: Mutation,
173
+ handlers: Handlers<Mutation, SuccessMessage, ErrorMessage>,
174
+ ) =>
175
+ (
176
+ ...args: Ref.OptionalArgs<Mutation>
177
+ ): Effect.Effect<SuccessMessage | ErrorMessage, never, Client.Client> =>
178
+ run(handlers, (client) => client.mutation(ref, ...args));
179
+
180
+ /**
181
+ * An execute body for a hand-written `Command.define` — every failure already
182
+ * folded into a Message. See `queryEffect`.
183
+ */
184
+ export const actionEffect =
185
+ <Action extends Ref.AnyConfectPublicAction, SuccessMessage, ErrorMessage>(
186
+ ref: Action,
187
+ handlers: Handlers<Action, SuccessMessage, ErrorMessage>,
188
+ ) =>
189
+ (
190
+ ...args: Ref.OptionalArgs<Action>
191
+ ): Effect.Effect<SuccessMessage | ErrorMessage, never, Client.Client> =>
192
+ run(handlers, (client) => client.action(ref, ...args));
193
+
194
+ type RunWithArgs<Ref_ extends Ref.AnyConfect, Message> = (
195
+ ...args: Ref.OptionalArgs<Ref_>
196
+ ) => Effect.Effect<Message, never, Client.Client>;
197
+
198
+ function makeDefinition<
199
+ Name extends string,
200
+ Ref_ extends Ref.AnyConfect,
201
+ SuccessMessage,
202
+ ErrorMessage,
203
+ KeyField extends keyof SchemaArgs<Ref_> & string = keyof SchemaArgs<Ref_> &
204
+ string,
205
+ >(
206
+ name: Name,
207
+ ref: Ref_,
208
+ messages: ReadonlyArray<Schema.Top>,
209
+ runWithArgs: RunWithArgs<Ref_, SuccessMessage | ErrorMessage>,
210
+ interrupt?: InterruptOption<Ref_, KeyField>,
211
+ ): Definition<Name, Ref_, SuccessMessage | ErrorMessage> {
212
+ type Fields = Ref.ArgsSchema<Ref_>["fields"];
213
+ type FoldkitArgs = SchemaArgs<Ref_>;
214
+
215
+ const fields: Fields = ref.args.fields;
216
+ const execute = (args: FoldkitArgs | undefined) =>
217
+ runWithArgs(
218
+ ...((args === undefined ? [] : [args]) as Ref.OptionalArgs<Ref_>),
219
+ );
220
+
221
+ return Match.value(interrupt).pipe(
222
+ Match.withReturnType<
223
+ Definition<Name, Ref_, SuccessMessage | ErrorMessage>
224
+ >(),
225
+ Match.when(
226
+ undefined,
227
+ () =>
228
+ FoldkitCommand.define(name, {
229
+ args: fields,
230
+ messages,
231
+ execute,
232
+ }) as Definition<Name, Ref_, SuccessMessage | ErrorMessage>,
233
+ ),
234
+ Match.when(
235
+ true,
236
+ (nameKeyedInterrupt) =>
237
+ FoldkitCommand.define(name, {
238
+ args: fields,
239
+ messages,
240
+ interrupt: nameKeyedInterrupt,
241
+ execute,
242
+ }) as InterruptibleDefinition<
243
+ Name,
244
+ Ref_,
245
+ SuccessMessage | ErrorMessage
246
+ >,
247
+ ),
248
+ Match.when(Match.defined, (keyedInterrupt) => {
249
+ const definition: FoldkitCommand.CommandDefinitionWithArgs<
250
+ Name,
251
+ Fields,
252
+ ReturnType<typeof execute>
253
+ > = FoldkitCommand.define(name, {
254
+ args: fields,
255
+ messages,
256
+ interrupt: keyedInterrupt,
257
+ execute,
258
+ });
259
+ return definition as Definition<
260
+ Name,
261
+ Ref_,
262
+ SuccessMessage | ErrorMessage
263
+ >;
264
+ }),
265
+ Match.exhaustive,
266
+ );
267
+ }
268
+
269
+ type FactoryConfig<
270
+ Ref_ extends Ref.AnyConfect,
271
+ Messages extends ReadonlyArray<Schema.Top>,
272
+ SuccessMessage,
273
+ ErrorMessage,
274
+ > = Handlers<Ref_, SuccessMessage, ErrorMessage> & {
275
+ readonly messages: Messages;
276
+ };
277
+
278
+ type KeyedFactoryConfig<
279
+ Ref_ extends Ref.AnyConfect,
280
+ Messages extends ReadonlyArray<Schema.Top>,
281
+ SuccessMessage,
282
+ ErrorMessage,
283
+ KeyField extends keyof SchemaArgs<Ref_> & string,
284
+ > = FactoryConfig<Ref_, Messages, SuccessMessage, ErrorMessage> & {
285
+ readonly interrupt: KeyedInterrupt<Ref_, KeyField>;
286
+ };
287
+
288
+ type NameKeyedFactoryConfig<
289
+ Ref_ extends Ref.AnyConfect,
290
+ Messages extends ReadonlyArray<Schema.Top>,
291
+ SuccessMessage,
292
+ ErrorMessage,
293
+ > = FactoryConfig<Ref_, Messages, SuccessMessage, ErrorMessage> & {
294
+ readonly interrupt: true;
295
+ };
296
+
297
+ type StandardFactoryConfig<
298
+ Ref_ extends Ref.AnyConfect,
299
+ Messages extends ReadonlyArray<Schema.Top>,
300
+ SuccessMessage,
301
+ ErrorMessage,
302
+ > = FactoryConfig<Ref_, Messages, SuccessMessage, ErrorMessage> & {
303
+ readonly interrupt?: never;
304
+ };
305
+
306
+ /**
307
+ * Builds the overload set for one ref kind. Each arm preserves whether
308
+ * `interrupt` is absent, `true`, or args-keyed all the way to
309
+ * `makeDefinition`.
310
+ */
311
+ const makeFactory = <BoundRef extends Ref.AnyConfect>(
312
+ effectHelper: <Ref_ extends BoundRef, SuccessMessage, ErrorMessage>(
313
+ ref: Ref_,
314
+ handlers: Handlers<Ref_, SuccessMessage, ErrorMessage>,
315
+ ) => RunWithArgs<Ref_, SuccessMessage | ErrorMessage>,
316
+ ) => {
317
+ function factory<
318
+ const Name extends string,
319
+ Ref_ extends BoundRef,
320
+ const Messages extends ReadonlyArray<Schema.Top>,
321
+ SuccessMessage extends Schema.Schema.Type<Messages[number]>,
322
+ ErrorMessage extends Schema.Schema.Type<Messages[number]>,
323
+ KeyField extends keyof SchemaArgs<Ref_> & string,
324
+ >(
325
+ name: Name,
326
+ ref: Ref_,
327
+ config: KeyedFactoryConfig<
328
+ Ref_,
329
+ Messages,
330
+ SuccessMessage,
331
+ ErrorMessage,
332
+ KeyField
333
+ >,
334
+ ): KeyedInterruptibleDefinition<
335
+ Name,
336
+ Ref_,
337
+ KeyField,
338
+ SuccessMessage | ErrorMessage
339
+ >;
340
+ function factory<
341
+ const Name extends string,
342
+ Ref_ extends BoundRef,
343
+ const Messages extends ReadonlyArray<Schema.Top>,
344
+ SuccessMessage extends Schema.Schema.Type<Messages[number]>,
345
+ ErrorMessage extends Schema.Schema.Type<Messages[number]>,
346
+ >(
347
+ name: Name,
348
+ ref: Ref_,
349
+ config: NameKeyedFactoryConfig<
350
+ Ref_,
351
+ Messages,
352
+ SuccessMessage,
353
+ ErrorMessage
354
+ >,
355
+ ): InterruptibleDefinition<Name, Ref_, SuccessMessage | ErrorMessage>;
356
+ function factory<
357
+ const Name extends string,
358
+ Ref_ extends BoundRef,
359
+ const Messages extends ReadonlyArray<Schema.Top>,
360
+ SuccessMessage extends Schema.Schema.Type<Messages[number]>,
361
+ ErrorMessage extends Schema.Schema.Type<Messages[number]>,
362
+ >(
363
+ name: Name,
364
+ ref: Ref_,
365
+ config: StandardFactoryConfig<Ref_, Messages, SuccessMessage, ErrorMessage>,
366
+ ): Definition<Name, Ref_, SuccessMessage | ErrorMessage>;
367
+ function factory<
368
+ const Name extends string,
369
+ Ref_ extends BoundRef,
370
+ SuccessMessage,
371
+ ErrorMessage,
372
+ KeyField extends keyof SchemaArgs<Ref_> & string,
373
+ >(
374
+ name: Name,
375
+ ref: Ref_,
376
+ config: FactoryConfig<
377
+ Ref_,
378
+ ReadonlyArray<Schema.Top>,
379
+ SuccessMessage,
380
+ ErrorMessage
381
+ > & {
382
+ readonly interrupt?: InterruptOption<Ref_, KeyField>;
383
+ },
384
+ ): Definition<Name, Ref_, SuccessMessage | ErrorMessage> {
385
+ const runWithArgs = effectHelper(ref, config);
386
+ return Match.value(config.interrupt).pipe(
387
+ Match.withReturnType<
388
+ Definition<Name, Ref_, SuccessMessage | ErrorMessage>
389
+ >(),
390
+ Match.when(undefined, () =>
391
+ makeDefinition(name, ref, config.messages, runWithArgs),
392
+ ),
393
+ Match.when(true, (interrupt) =>
394
+ makeDefinition(name, ref, config.messages, runWithArgs, interrupt),
395
+ ),
396
+ Match.when(Match.defined, (interrupt) =>
397
+ makeDefinition(name, ref, config.messages, runWithArgs, interrupt),
398
+ ),
399
+ Match.exhaustive,
400
+ );
401
+ }
402
+
403
+ return factory;
404
+ };
405
+
406
+ /**
407
+ * A Foldkit Command definition for a Confect query whose Command args are the
408
+ * ref's args:
409
+ *
410
+ * ```ts
411
+ * const FetchNote = Command.query("FetchNote", refs.public.notes.get, {
412
+ * messages: [SucceededGetNote, FailedGetNote],
413
+ * onSuccess: (note) => SucceededGetNote({ note }),
414
+ * onError: (error) => FailedGetNote({ message: String(error) }),
415
+ * })
416
+ * // In update:
417
+ * // [model, [FetchNote({ noteId })]]
418
+ * ```
419
+ *
420
+ * Pass `interrupt` to make invocations interruptible via the returned
421
+ * definition's `Interrupt` constructor — `true` keys them by the Command
422
+ * name, `{ keyFields, toKey }` by a part derived from the ref's args:
423
+ *
424
+ * ```ts
425
+ * const SaveDraft = Command.mutation("SaveDraft", refs.public.notes.insert, {
426
+ * messages: [SucceededSaveDraft, FailedSaveDraft],
427
+ * onSuccess: (noteId) => SucceededSaveDraft({ noteId }),
428
+ * onError: (error) => FailedSaveDraft({ message: String(error) }),
429
+ * interrupt: true,
430
+ * })
431
+ * // In update:
432
+ * // [model, [SaveDraft.Interrupt((outcome) => CompletedCancelSaveDraft({ outcome }))]]
433
+ * ```
434
+ */
435
+ export const query: ReturnType<typeof makeFactory<Ref.AnyConfectPublicQuery>> =
436
+ makeFactory(queryEffect);
437
+
438
+ /**
439
+ * A Foldkit Command definition for a Confect mutation whose Command args are
440
+ * the ref's args. See `query`.
441
+ */
442
+ export const mutation: ReturnType<
443
+ typeof makeFactory<Ref.AnyConfectPublicMutation>
444
+ > = makeFactory(mutationEffect);
445
+
446
+ /**
447
+ * A Foldkit Command definition for a Confect action whose Command args are
448
+ * the ref's args. See `query`.
449
+ */
450
+ export const action: ReturnType<
451
+ typeof makeFactory<Ref.AnyConfectPublicAction>
452
+ > = makeFactory(actionEffect);