@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/CHANGELOG.md +61 -0
- package/LICENSE +7 -0
- package/README.md +11 -0
- package/dist/Client.d.ts +69 -0
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +44 -0
- package/dist/Client.js.map +1 -0
- package/dist/Command.d.ts +158 -0
- package/dist/Command.d.ts.map +1 -0
- package/dist/Command.js +111 -0
- package/dist/Command.js.map +1 -0
- package/dist/PaginatedQuery.d.ts +227 -0
- package/dist/PaginatedQuery.d.ts.map +1 -0
- package/dist/PaginatedQuery.js +351 -0
- package/dist/PaginatedQuery.js.map +1 -0
- package/dist/Subscription.d.ts +132 -0
- package/dist/Subscription.d.ts.map +1 -0
- package/dist/Subscription.js +119 -0
- package/dist/Subscription.js.map +1 -0
- package/dist/WebSocketClient.d.ts +16 -0
- package/dist/WebSocketClient.d.ts.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/package.json +71 -0
- package/src/Client.ts +53 -0
- package/src/Command.ts +452 -0
- package/src/PaginatedQuery.ts +1001 -0
- package/src/Subscription.ts +338 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
import * as MiddlewareSpec from "@confect/core/MiddlewareSpec";
|
|
2
|
+
import * as PaginationError from "@confect/core/PaginationError";
|
|
3
|
+
import * as Ref from "@confect/core/Ref";
|
|
4
|
+
import * as Data from "effect/Data";
|
|
5
|
+
import * as Function from "effect/Function";
|
|
6
|
+
import * as Match from "effect/Match";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as Result from "effect/Result";
|
|
9
|
+
import * as Schema from "effect/Schema";
|
|
10
|
+
import * as SchemaGetter from "effect/SchemaGetter";
|
|
11
|
+
import * as SchemaIssue from "effect/SchemaIssue";
|
|
12
|
+
import * as Client from "./Client";
|
|
13
|
+
|
|
14
|
+
/** Identifies the cursor range fetched for one page. */
|
|
15
|
+
export interface PageDescriptor {
|
|
16
|
+
readonly cursor: string | null;
|
|
17
|
+
readonly endCursor: Option.Option<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const PageDescriptor = Schema.Struct({
|
|
21
|
+
cursor: Schema.Union([Schema.String, Schema.Null]),
|
|
22
|
+
endCursor: Schema.Option(Schema.String),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Options that remain fixed for one pagination session. */
|
|
26
|
+
export interface Options {
|
|
27
|
+
readonly initialNumItems: number;
|
|
28
|
+
readonly maximumRowsRead?: number;
|
|
29
|
+
readonly maximumBytesRead?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0));
|
|
33
|
+
const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
34
|
+
|
|
35
|
+
export const Options = Schema.Struct({
|
|
36
|
+
initialNumItems: PositiveInt,
|
|
37
|
+
maximumRowsRead: Schema.optionalKey(PositiveInt),
|
|
38
|
+
maximumBytesRead: Schema.optionalKey(PositiveInt),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** Why the machine is fetching its current target. */
|
|
42
|
+
export type Direction =
|
|
43
|
+
| "Initial"
|
|
44
|
+
| "Next"
|
|
45
|
+
| "Previous"
|
|
46
|
+
| "First"
|
|
47
|
+
| "Split"
|
|
48
|
+
| "Reset";
|
|
49
|
+
|
|
50
|
+
/** A complete page that can remain on screen while another page is fetched. */
|
|
51
|
+
export interface Page<Item_> {
|
|
52
|
+
readonly descriptor: PageDescriptor;
|
|
53
|
+
readonly number: number;
|
|
54
|
+
readonly items: ReadonlyArray<Item_>;
|
|
55
|
+
readonly continueCursor: string;
|
|
56
|
+
readonly isDone: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type Loading = Data.TaggedEnum<{
|
|
60
|
+
Loading: { readonly direction: Direction };
|
|
61
|
+
}>;
|
|
62
|
+
|
|
63
|
+
export type Refreshing<Item_> = Data.TaggedEnum<{
|
|
64
|
+
Refreshing: { readonly data: Page<Item_>; readonly direction: Direction };
|
|
65
|
+
}>;
|
|
66
|
+
|
|
67
|
+
export type Success<Item_> = Data.TaggedEnum<{
|
|
68
|
+
Success: { readonly data: Page<Item_> };
|
|
69
|
+
}>;
|
|
70
|
+
|
|
71
|
+
export type Failure<Error_> = Data.TaggedEnum<{
|
|
72
|
+
Failure: { readonly error: Error_ };
|
|
73
|
+
}>;
|
|
74
|
+
|
|
75
|
+
export type Stale<Item_, Error_> = Data.TaggedEnum<{
|
|
76
|
+
Stale: { readonly data: Page<Item_>; readonly error: Error_ };
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
export type Phase<Item_, Error_> =
|
|
80
|
+
| Loading
|
|
81
|
+
| Refreshing<Item_>
|
|
82
|
+
| Success<Item_>
|
|
83
|
+
| Failure<Error_>
|
|
84
|
+
| Stale<Item_, Error_>;
|
|
85
|
+
|
|
86
|
+
interface PhaseDefinition extends Data.TaggedEnum.WithGenerics<2> {
|
|
87
|
+
readonly taggedEnum: Phase<this["A"], this["B"]>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const Phase = Data.taggedEnum<PhaseDefinition>();
|
|
91
|
+
|
|
92
|
+
/** A closed machine that retains its generation for stale-result rejection. */
|
|
93
|
+
export type Idle = Data.TaggedEnum<{
|
|
94
|
+
Idle: { readonly generation: number };
|
|
95
|
+
}>;
|
|
96
|
+
|
|
97
|
+
/** A live cursor-pagination session. */
|
|
98
|
+
export type Active<Item_, UserArgs_, Error_> = Data.TaggedEnum<{
|
|
99
|
+
Active: {
|
|
100
|
+
readonly generation: number;
|
|
101
|
+
readonly args: UserArgs_;
|
|
102
|
+
readonly options: Options;
|
|
103
|
+
readonly paginationId: Option.Option<number>;
|
|
104
|
+
readonly requestId: number;
|
|
105
|
+
readonly current: PageDescriptor;
|
|
106
|
+
readonly prevStack: ReadonlyArray<PageDescriptor>;
|
|
107
|
+
readonly phase: Phase<Item_, Error_>;
|
|
108
|
+
};
|
|
109
|
+
}>;
|
|
110
|
+
|
|
111
|
+
/** A schema-backed cursor-pagination machine. */
|
|
112
|
+
export type State<Item_, UserArgs_, Error_> =
|
|
113
|
+
| Idle
|
|
114
|
+
| Active<Item_, UserArgs_, Error_>;
|
|
115
|
+
|
|
116
|
+
interface StateDefinition extends Data.TaggedEnum.WithGenerics<3> {
|
|
117
|
+
readonly taggedEnum: State<this["A"], this["B"], this["C"]>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const State = Data.taggedEnum<StateDefinition>();
|
|
121
|
+
|
|
122
|
+
/** The logical request from which a subscription allocates a session id. */
|
|
123
|
+
export interface SubscriptionRequest<UserArgs_> {
|
|
124
|
+
readonly generation: number;
|
|
125
|
+
readonly args: UserArgs_;
|
|
126
|
+
readonly options: Options;
|
|
127
|
+
readonly paginationId: Option.Option<number>;
|
|
128
|
+
readonly requestId: number;
|
|
129
|
+
readonly descriptor: PageDescriptor;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The complete identity and arguments of a subscribed page request. */
|
|
133
|
+
export interface Request<UserArgs_> {
|
|
134
|
+
readonly generation: number;
|
|
135
|
+
readonly args: UserArgs_;
|
|
136
|
+
readonly options: Options;
|
|
137
|
+
readonly paginationId: number;
|
|
138
|
+
readonly requestId: number;
|
|
139
|
+
readonly descriptor: PageDescriptor;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The successful payload returned by a Convex paginated query. */
|
|
143
|
+
export interface PageResult<Item_> {
|
|
144
|
+
readonly page: ReadonlyArray<Item_>;
|
|
145
|
+
readonly isDone: boolean;
|
|
146
|
+
readonly continueCursor: string;
|
|
147
|
+
readonly splitCursor?: string | null;
|
|
148
|
+
readonly pageStatus?: "SplitRecommended" | "SplitRequired" | null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* One correlated subscription outcome. Both success and failure travel
|
|
153
|
+
* through this value so `settle` can mirror `AsyncData.settle`.
|
|
154
|
+
*/
|
|
155
|
+
export interface Settlement<Item_, UserArgs_, Error_> {
|
|
156
|
+
readonly request: Request<UserArgs_>;
|
|
157
|
+
readonly result: Result.Result<PageResult<Item_>, Error_>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type Item<Query extends Ref.AnyConfectPublicPaginatedQuery> =
|
|
161
|
+
Ref.Returns<Query>["page"][number];
|
|
162
|
+
|
|
163
|
+
export type UserArgs<Query extends Ref.AnyConfectPublicPaginatedQuery> = Omit<
|
|
164
|
+
Ref.Args<Query>,
|
|
165
|
+
"paginationOpts"
|
|
166
|
+
>;
|
|
167
|
+
|
|
168
|
+
type TaggedFunctionError<Error_> = Data.TaggedEnum<{
|
|
169
|
+
FunctionError: { readonly error: Error_ };
|
|
170
|
+
}>;
|
|
171
|
+
|
|
172
|
+
interface FunctionErrorDefinition extends Data.TaggedEnum.WithGenerics<1> {
|
|
173
|
+
readonly taggedEnum: TaggedFunctionError<this["A"]>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Constructs a declared function or middleware failure envelope. */
|
|
177
|
+
export const FunctionError =
|
|
178
|
+
Data.taggedEnum<FunctionErrorDefinition>().FunctionError;
|
|
179
|
+
|
|
180
|
+
/** A declared function or middleware failure, kept distinct from client errors. */
|
|
181
|
+
export type FunctionError<Query extends Ref.AnyConfectPublicPaginatedQuery> = [
|
|
182
|
+
Ref.Error<Query>,
|
|
183
|
+
] extends [never]
|
|
184
|
+
? never
|
|
185
|
+
: TaggedFunctionError<Ref.Error<Query>>;
|
|
186
|
+
|
|
187
|
+
/** Every failure a paginated query subscription can settle with. */
|
|
188
|
+
export type Error<Query extends Ref.AnyConfectPublicPaginatedQuery> =
|
|
189
|
+
| FunctionError<Query>
|
|
190
|
+
| PaginationError.InvalidCursor
|
|
191
|
+
| Client.WebSocketClientError
|
|
192
|
+
| Schema.SchemaError;
|
|
193
|
+
|
|
194
|
+
export { InvalidCursor } from "@confect/core/PaginationError";
|
|
195
|
+
|
|
196
|
+
type MachineState<Query extends Ref.AnyConfectPublicPaginatedQuery> = State<
|
|
197
|
+
Item<Query>,
|
|
198
|
+
UserArgs<Query>,
|
|
199
|
+
Error<Query>
|
|
200
|
+
>;
|
|
201
|
+
|
|
202
|
+
type MachineActive<Query extends Ref.AnyConfectPublicPaginatedQuery> = Active<
|
|
203
|
+
Item<Query>,
|
|
204
|
+
UserArgs<Query>,
|
|
205
|
+
Error<Query>
|
|
206
|
+
>;
|
|
207
|
+
|
|
208
|
+
/** The schema-and-constructor bundle returned by `make`. */
|
|
209
|
+
export interface PaginatedQuery<
|
|
210
|
+
Query extends Ref.AnyConfectPublicPaginatedQuery,
|
|
211
|
+
> {
|
|
212
|
+
readonly ref: Query;
|
|
213
|
+
readonly schema: Schema.Codec<MachineState<Query>, unknown>;
|
|
214
|
+
readonly idle: Idle;
|
|
215
|
+
readonly subscriptionRequestSchema: Schema.Codec<
|
|
216
|
+
SubscriptionRequest<UserArgs<Query>>,
|
|
217
|
+
unknown
|
|
218
|
+
>;
|
|
219
|
+
readonly requestSchema: Schema.Codec<Request<UserArgs<Query>>, unknown>;
|
|
220
|
+
readonly settlement: Schema.Codec<
|
|
221
|
+
Settlement<Item<Query>, UserArgs<Query>, Error<Query>>,
|
|
222
|
+
unknown
|
|
223
|
+
>;
|
|
224
|
+
readonly init: keyof UserArgs<Query> extends never
|
|
225
|
+
? (state: Idle, options: Options) => MachineActive<Query>
|
|
226
|
+
: (
|
|
227
|
+
state: Idle,
|
|
228
|
+
args: UserArgs<Query>,
|
|
229
|
+
options: Options,
|
|
230
|
+
) => MachineActive<Query>;
|
|
231
|
+
readonly reinitialize: keyof UserArgs<Query> extends never
|
|
232
|
+
? (state: MachineActive<Query>, options?: Options) => MachineActive<Query>
|
|
233
|
+
: (
|
|
234
|
+
state: MachineActive<Query>,
|
|
235
|
+
args: UserArgs<Query>,
|
|
236
|
+
options?: Options,
|
|
237
|
+
) => MachineActive<Query>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type Any = PaginatedQuery<Ref.AnyConfectPublicPaginatedQuery>;
|
|
241
|
+
|
|
242
|
+
const firstDescriptor = (): PageDescriptor => ({
|
|
243
|
+
cursor: null,
|
|
244
|
+
endCursor: Option.none(),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const decodeOptions = Schema.decodeUnknownSync(Options);
|
|
248
|
+
|
|
249
|
+
const initialState = <Item_, UserArgs_, Error_>(
|
|
250
|
+
generation: number,
|
|
251
|
+
args: UserArgs_,
|
|
252
|
+
options: Options,
|
|
253
|
+
): Active<Item_, UserArgs_, Error_> =>
|
|
254
|
+
State.Active<Item_, UserArgs_, Error_>({
|
|
255
|
+
generation,
|
|
256
|
+
args,
|
|
257
|
+
options: decodeOptions(options),
|
|
258
|
+
paginationId: Option.none(),
|
|
259
|
+
requestId: 1,
|
|
260
|
+
current: firstDescriptor(),
|
|
261
|
+
prevStack: [],
|
|
262
|
+
phase: Phase.Loading<Item_, Error_>({ direction: "Initial" }),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const missingPaginatedProvenanceError = (ref: Ref.AnyConfect) =>
|
|
266
|
+
new globalThis.Error(
|
|
267
|
+
`Paginated query ref "${Ref.getConvexFunctionName(ref)}" was not built ` +
|
|
268
|
+
"with `FunctionSpec.publicPaginatedQuery`. `PaginatedQuery.make` " +
|
|
269
|
+
"requires the user-args and item schemas that constructor stores.",
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const paginatedKindOrThrow = (ref: Ref.AnyConfectPublicPaginatedQuery) =>
|
|
273
|
+
Match.value(ref.kind).pipe(
|
|
274
|
+
Match.tag("Paginated", (kind) => kind),
|
|
275
|
+
Match.tag("Standard", () => {
|
|
276
|
+
throw missingPaginatedProvenanceError(ref);
|
|
277
|
+
}),
|
|
278
|
+
Match.exhaustive,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const SchemaErrorJson = Schema.TaggedStruct("SchemaError", {
|
|
282
|
+
message: Schema.String,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const SerializableSchemaError = Schema.instanceOf(Schema.SchemaError, {
|
|
286
|
+
toCodecJson: () =>
|
|
287
|
+
Schema.link<Schema.SchemaError>()(SchemaErrorJson, {
|
|
288
|
+
decode: SchemaGetter.transform(
|
|
289
|
+
({ message }) =>
|
|
290
|
+
new Schema.SchemaError(new SchemaIssue.InvalidValue({ message })),
|
|
291
|
+
),
|
|
292
|
+
encode: SchemaGetter.transform((error) =>
|
|
293
|
+
SchemaErrorJson.make({ message: error.message }),
|
|
294
|
+
),
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
/** Builds a page machine from a paginated ref. */
|
|
299
|
+
export const make = <Query extends Ref.AnyConfectPublicPaginatedQuery>(
|
|
300
|
+
ref: Query,
|
|
301
|
+
): PaginatedQuery<Query> => {
|
|
302
|
+
const kind = paginatedKindOrThrow(ref);
|
|
303
|
+
const hasUserArgs = globalThis.Object.keys(kind.userArgs.fields).length > 0;
|
|
304
|
+
const functionErrorSchemas = [
|
|
305
|
+
...("error" in ref && ref.error !== undefined ? [ref.error] : []),
|
|
306
|
+
...MiddlewareSpec.errorSchemas(ref.middlewareSpecs),
|
|
307
|
+
];
|
|
308
|
+
const errorSchema = Schema.Union([
|
|
309
|
+
PaginationError.InvalidCursor,
|
|
310
|
+
Client.WebSocketClientError,
|
|
311
|
+
SerializableSchemaError,
|
|
312
|
+
...(functionErrorSchemas.length === 0
|
|
313
|
+
? []
|
|
314
|
+
: [
|
|
315
|
+
Schema.TaggedStruct("FunctionError", {
|
|
316
|
+
error:
|
|
317
|
+
functionErrorSchemas.length === 1
|
|
318
|
+
? functionErrorSchemas[0]!
|
|
319
|
+
: Schema.Union(functionErrorSchemas),
|
|
320
|
+
}),
|
|
321
|
+
]),
|
|
322
|
+
]) as Schema.Codec<Error<Query>, unknown>;
|
|
323
|
+
const directions = Schema.Literals([
|
|
324
|
+
"Initial",
|
|
325
|
+
"Next",
|
|
326
|
+
"Previous",
|
|
327
|
+
"First",
|
|
328
|
+
"Split",
|
|
329
|
+
"Reset",
|
|
330
|
+
]);
|
|
331
|
+
const page = Schema.Struct({
|
|
332
|
+
descriptor: PageDescriptor,
|
|
333
|
+
number: PositiveInt,
|
|
334
|
+
items: kind.page,
|
|
335
|
+
continueCursor: Schema.String,
|
|
336
|
+
isDone: Schema.Boolean,
|
|
337
|
+
});
|
|
338
|
+
const phase = Schema.TaggedUnion({
|
|
339
|
+
Loading: { direction: directions },
|
|
340
|
+
Refreshing: { data: page, direction: directions },
|
|
341
|
+
Success: { data: page },
|
|
342
|
+
Failure: { error: errorSchema },
|
|
343
|
+
Stale: { data: page, error: errorSchema },
|
|
344
|
+
});
|
|
345
|
+
const subscriptionRequestSchema: PaginatedQuery<Query>["subscriptionRequestSchema"] =
|
|
346
|
+
Schema.Struct({
|
|
347
|
+
generation: PositiveInt,
|
|
348
|
+
args: kind.userArgs,
|
|
349
|
+
options: Options,
|
|
350
|
+
paginationId: Schema.Option(PositiveInt),
|
|
351
|
+
requestId: PositiveInt,
|
|
352
|
+
descriptor: PageDescriptor,
|
|
353
|
+
});
|
|
354
|
+
const requestSchema: PaginatedQuery<Query>["requestSchema"] = Schema.Struct({
|
|
355
|
+
generation: PositiveInt,
|
|
356
|
+
args: kind.userArgs,
|
|
357
|
+
options: Options,
|
|
358
|
+
paginationId: PositiveInt,
|
|
359
|
+
requestId: PositiveInt,
|
|
360
|
+
descriptor: PageDescriptor,
|
|
361
|
+
});
|
|
362
|
+
const pageResult = Schema.Struct({
|
|
363
|
+
page: kind.page,
|
|
364
|
+
isDone: Schema.Boolean,
|
|
365
|
+
continueCursor: Schema.String,
|
|
366
|
+
splitCursor: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])),
|
|
367
|
+
pageStatus: Schema.optionalKey(
|
|
368
|
+
Schema.Union([
|
|
369
|
+
Schema.Literal("SplitRecommended"),
|
|
370
|
+
Schema.Literal("SplitRequired"),
|
|
371
|
+
Schema.Null,
|
|
372
|
+
]),
|
|
373
|
+
),
|
|
374
|
+
});
|
|
375
|
+
const schema: PaginatedQuery<Query>["schema"] = Schema.TaggedUnion({
|
|
376
|
+
Idle: { generation: NonNegativeInt },
|
|
377
|
+
Active: {
|
|
378
|
+
generation: PositiveInt,
|
|
379
|
+
args: kind.userArgs,
|
|
380
|
+
options: Options,
|
|
381
|
+
paginationId: Schema.Option(PositiveInt),
|
|
382
|
+
requestId: PositiveInt,
|
|
383
|
+
current: PageDescriptor,
|
|
384
|
+
prevStack: Schema.Array(PageDescriptor),
|
|
385
|
+
phase,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
const settlement: PaginatedQuery<Query>["settlement"] = Schema.Struct({
|
|
389
|
+
request: requestSchema,
|
|
390
|
+
result: Schema.Result(pageResult, errorSchema),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const init: PaginatedQuery<Query>["init"] = (
|
|
394
|
+
state: Idle,
|
|
395
|
+
argsOrOptions: UserArgs<Query> | Options,
|
|
396
|
+
options?: Options,
|
|
397
|
+
): MachineActive<Query> =>
|
|
398
|
+
Match.value(options).pipe(
|
|
399
|
+
Match.withReturnType<MachineActive<Query>>(),
|
|
400
|
+
Match.when(undefined, () =>
|
|
401
|
+
initialState(
|
|
402
|
+
state.generation + 1,
|
|
403
|
+
{} as UserArgs<Query>,
|
|
404
|
+
argsOrOptions as Options,
|
|
405
|
+
),
|
|
406
|
+
),
|
|
407
|
+
Match.when(Match.defined, (definedOptions) =>
|
|
408
|
+
initialState(
|
|
409
|
+
state.generation + 1,
|
|
410
|
+
argsOrOptions as UserArgs<Query>,
|
|
411
|
+
definedOptions,
|
|
412
|
+
),
|
|
413
|
+
),
|
|
414
|
+
Match.exhaustive,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const reinitialize: PaginatedQuery<Query>["reinitialize"] = (
|
|
418
|
+
state: MachineActive<Query>,
|
|
419
|
+
argsOrOptions?: UserArgs<Query> | Options,
|
|
420
|
+
options?: Options,
|
|
421
|
+
): MachineActive<Query> =>
|
|
422
|
+
Match.value(options).pipe(
|
|
423
|
+
Match.withReturnType<MachineActive<Query>>(),
|
|
424
|
+
Match.when(Match.defined, (definedOptions) =>
|
|
425
|
+
initialState(
|
|
426
|
+
state.generation + 1,
|
|
427
|
+
argsOrOptions as UserArgs<Query>,
|
|
428
|
+
definedOptions,
|
|
429
|
+
),
|
|
430
|
+
),
|
|
431
|
+
Match.when(undefined, () =>
|
|
432
|
+
Match.value(argsOrOptions).pipe(
|
|
433
|
+
Match.withReturnType<MachineActive<Query>>(),
|
|
434
|
+
Match.when(undefined, () =>
|
|
435
|
+
initialState(state.generation + 1, state.args, state.options),
|
|
436
|
+
),
|
|
437
|
+
Match.when(Match.defined, (definedArgsOrOptions) =>
|
|
438
|
+
Match.value(hasUserArgs).pipe(
|
|
439
|
+
Match.withReturnType<MachineActive<Query>>(),
|
|
440
|
+
Match.when(true, () =>
|
|
441
|
+
initialState(
|
|
442
|
+
state.generation + 1,
|
|
443
|
+
definedArgsOrOptions as UserArgs<Query>,
|
|
444
|
+
state.options,
|
|
445
|
+
),
|
|
446
|
+
),
|
|
447
|
+
Match.when(false, () =>
|
|
448
|
+
initialState(
|
|
449
|
+
state.generation + 1,
|
|
450
|
+
state.args,
|
|
451
|
+
definedArgsOrOptions as Options,
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
Match.exhaustive,
|
|
455
|
+
),
|
|
456
|
+
),
|
|
457
|
+
Match.exhaustive,
|
|
458
|
+
),
|
|
459
|
+
),
|
|
460
|
+
Match.exhaustive,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
ref,
|
|
465
|
+
schema,
|
|
466
|
+
idle: State.Idle({ generation: 0 }),
|
|
467
|
+
subscriptionRequestSchema,
|
|
468
|
+
requestSchema,
|
|
469
|
+
settlement,
|
|
470
|
+
init,
|
|
471
|
+
reinitialize,
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const descriptorEquals = (a: PageDescriptor, b: PageDescriptor): boolean =>
|
|
476
|
+
a.cursor === b.cursor &&
|
|
477
|
+
Option.match(a.endCursor, {
|
|
478
|
+
onNone: () => Option.isNone(b.endCursor),
|
|
479
|
+
onSome: (aEnd) =>
|
|
480
|
+
Option.match(b.endCursor, {
|
|
481
|
+
onNone: () => false,
|
|
482
|
+
onSome: (bEnd) => aEnd === bEnd,
|
|
483
|
+
}),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
/** Returns the logical request currently driving the subscription. */
|
|
487
|
+
export const getSubscriptionRequest = <Item_, UserArgs_, Error_>(
|
|
488
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
489
|
+
): SubscriptionRequest<UserArgs_> => ({
|
|
490
|
+
generation: state.generation,
|
|
491
|
+
args: state.args,
|
|
492
|
+
options: state.options,
|
|
493
|
+
paginationId: state.paginationId,
|
|
494
|
+
requestId: state.requestId,
|
|
495
|
+
descriptor: state.current,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
/** Attaches the client-allocated pagination id to a logical request. */
|
|
499
|
+
export const allocateRequest = <UserArgs_>(
|
|
500
|
+
request: SubscriptionRequest<UserArgs_>,
|
|
501
|
+
paginationId: number,
|
|
502
|
+
): Request<UserArgs_> => ({
|
|
503
|
+
...request,
|
|
504
|
+
paginationId,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const phaseData = <Item_, Error_>(
|
|
508
|
+
phase: Phase<Item_, Error_>,
|
|
509
|
+
): Option.Option<Page<Item_>> =>
|
|
510
|
+
Match.value(phase).pipe(
|
|
511
|
+
Match.withReturnType<Option.Option<Page<Item_>>>(),
|
|
512
|
+
Match.tag("Refreshing", "Success", "Stale", (dataPhase) =>
|
|
513
|
+
Option.some(dataPhase.data),
|
|
514
|
+
),
|
|
515
|
+
Match.tag("Loading", "Failure", () => Option.none()),
|
|
516
|
+
Match.exhaustive,
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const pendingPhase = <Item_>(
|
|
520
|
+
data: Option.Option<Page<Item_>>,
|
|
521
|
+
direction: Direction,
|
|
522
|
+
): Loading | Refreshing<Item_> =>
|
|
523
|
+
Option.match(data, {
|
|
524
|
+
onNone: () => Phase.Loading<Item_, never>({ direction }),
|
|
525
|
+
onSome: (page) => Phase.Refreshing<Item_, never>({ data: page, direction }),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
/** Close the subscription while retaining its generation tombstone. */
|
|
529
|
+
export const close = <Item_, UserArgs_, Error_>(
|
|
530
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
531
|
+
): Idle => State.Idle({ generation: state.generation });
|
|
532
|
+
|
|
533
|
+
/** Start a fresh pagination session at page one while retaining visible data. */
|
|
534
|
+
export const reset = <Item_, UserArgs_, Error_>(
|
|
535
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
536
|
+
): Active<Item_, UserArgs_, Error_> => ({
|
|
537
|
+
...state,
|
|
538
|
+
generation: state.generation + 1,
|
|
539
|
+
paginationId: Option.none(),
|
|
540
|
+
requestId: 1,
|
|
541
|
+
current: firstDescriptor(),
|
|
542
|
+
prevStack: [],
|
|
543
|
+
phase: pendingPhase(phaseData(state.phase), "Reset"),
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
/** Navigate to the next page, retaining the current page while it loads. */
|
|
547
|
+
export const next = <Item_, UserArgs_, Error_>(
|
|
548
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
549
|
+
): Option.Option<Active<Item_, UserArgs_, Error_>> =>
|
|
550
|
+
Match.value(state.phase).pipe(
|
|
551
|
+
Match.withReturnType<Option.Option<Active<Item_, UserArgs_, Error_>>>(),
|
|
552
|
+
Match.tag("Success", ({ data }) =>
|
|
553
|
+
Match.value(data.isDone).pipe(
|
|
554
|
+
Match.withReturnType<Option.Option<Active<Item_, UserArgs_, Error_>>>(),
|
|
555
|
+
Match.when(true, () => Option.none()),
|
|
556
|
+
Match.when(false, () =>
|
|
557
|
+
Option.some({
|
|
558
|
+
...state,
|
|
559
|
+
requestId: state.requestId + 1,
|
|
560
|
+
current: {
|
|
561
|
+
cursor: Option.getOrElse(
|
|
562
|
+
data.descriptor.endCursor,
|
|
563
|
+
() => data.continueCursor,
|
|
564
|
+
),
|
|
565
|
+
endCursor: Option.none(),
|
|
566
|
+
},
|
|
567
|
+
prevStack: [...state.prevStack, data.descriptor],
|
|
568
|
+
phase: Phase.Refreshing<Item_, Error_>({
|
|
569
|
+
data,
|
|
570
|
+
direction: "Next",
|
|
571
|
+
}),
|
|
572
|
+
}),
|
|
573
|
+
),
|
|
574
|
+
Match.exhaustive,
|
|
575
|
+
),
|
|
576
|
+
),
|
|
577
|
+
Match.tag("Loading", "Refreshing", "Failure", "Stale", () => Option.none()),
|
|
578
|
+
Match.exhaustive,
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
/** Navigate to the previous page, retaining the current page while it loads. */
|
|
582
|
+
export const prev = <Item_, UserArgs_, Error_>(
|
|
583
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
584
|
+
): Option.Option<Active<Item_, UserArgs_, Error_>> =>
|
|
585
|
+
Match.value(state.phase).pipe(
|
|
586
|
+
Match.withReturnType<Option.Option<Active<Item_, UserArgs_, Error_>>>(),
|
|
587
|
+
Match.tag("Success", ({ data }) => {
|
|
588
|
+
const current = state.prevStack.at(-1);
|
|
589
|
+
return Match.value(current).pipe(
|
|
590
|
+
Match.withReturnType<Option.Option<Active<Item_, UserArgs_, Error_>>>(),
|
|
591
|
+
Match.when(undefined, () => Option.none()),
|
|
592
|
+
Match.when(Match.defined, (previous) =>
|
|
593
|
+
Option.some({
|
|
594
|
+
...state,
|
|
595
|
+
requestId: state.requestId + 1,
|
|
596
|
+
current: previous,
|
|
597
|
+
prevStack: state.prevStack.slice(0, -1),
|
|
598
|
+
phase: Phase.Refreshing<Item_, Error_>({
|
|
599
|
+
data,
|
|
600
|
+
direction: "Previous",
|
|
601
|
+
}),
|
|
602
|
+
}),
|
|
603
|
+
),
|
|
604
|
+
Match.exhaustive,
|
|
605
|
+
);
|
|
606
|
+
}),
|
|
607
|
+
Match.tag("Loading", "Refreshing", "Failure", "Stale", () => Option.none()),
|
|
608
|
+
Match.exhaustive,
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
/** Navigate to page one without invalidating the current pagination session. */
|
|
612
|
+
export const first = <Item_, UserArgs_, Error_>(
|
|
613
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
614
|
+
): Option.Option<Active<Item_, UserArgs_, Error_>> =>
|
|
615
|
+
Match.value(
|
|
616
|
+
state.prevStack.length === 0 &&
|
|
617
|
+
descriptorEquals(state.current, firstDescriptor()),
|
|
618
|
+
).pipe(
|
|
619
|
+
Match.withReturnType<Option.Option<Active<Item_, UserArgs_, Error_>>>(),
|
|
620
|
+
Match.when(true, () => Option.none()),
|
|
621
|
+
Match.when(false, () =>
|
|
622
|
+
Option.some({
|
|
623
|
+
...state,
|
|
624
|
+
requestId: state.requestId + 1,
|
|
625
|
+
current: firstDescriptor(),
|
|
626
|
+
prevStack: [],
|
|
627
|
+
phase: pendingPhase(phaseData(state.phase), "First"),
|
|
628
|
+
}),
|
|
629
|
+
),
|
|
630
|
+
Match.exhaustive,
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
/** Whether a request still names the machine's live pagination session/page. */
|
|
634
|
+
export const isCurrentRequest = <Item_, UserArgs_, Error_>(
|
|
635
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
636
|
+
candidate: Request<UserArgs_>,
|
|
637
|
+
): boolean =>
|
|
638
|
+
Match.value(state).pipe(
|
|
639
|
+
Match.tag("Idle", () => false),
|
|
640
|
+
Match.tag("Active", (active) =>
|
|
641
|
+
Match.value(active.paginationId).pipe(
|
|
642
|
+
Match.tag("None", () =>
|
|
643
|
+
Match.value(
|
|
644
|
+
active.generation === candidate.generation &&
|
|
645
|
+
active.requestId === candidate.requestId &&
|
|
646
|
+
descriptorEquals(active.current, candidate.descriptor),
|
|
647
|
+
).pipe(
|
|
648
|
+
Match.when(true, () => true),
|
|
649
|
+
Match.when(false, () => false),
|
|
650
|
+
Match.exhaustive,
|
|
651
|
+
),
|
|
652
|
+
),
|
|
653
|
+
Match.tag(
|
|
654
|
+
"Some",
|
|
655
|
+
({ value }) =>
|
|
656
|
+
active.generation === candidate.generation &&
|
|
657
|
+
value === candidate.paginationId &&
|
|
658
|
+
active.requestId === candidate.requestId &&
|
|
659
|
+
descriptorEquals(active.current, candidate.descriptor),
|
|
660
|
+
),
|
|
661
|
+
Match.exhaustive,
|
|
662
|
+
),
|
|
663
|
+
),
|
|
664
|
+
Match.exhaustive,
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const pageFromResult = <Item_, UserArgs_, Error_>(
|
|
668
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
669
|
+
result: PageResult<Item_>,
|
|
670
|
+
): Page<Item_> => ({
|
|
671
|
+
descriptor: state.current,
|
|
672
|
+
number: state.prevStack.length + 1,
|
|
673
|
+
items: result.page,
|
|
674
|
+
continueCursor: result.continueCursor,
|
|
675
|
+
isDone: result.isDone,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const settleSuccess = <Item_, UserArgs_, Error_>(
|
|
679
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
680
|
+
result: PageResult<Item_>,
|
|
681
|
+
): Active<Item_, UserArgs_, Error_> => {
|
|
682
|
+
const splitCursor = result.splitCursor;
|
|
683
|
+
const splitSignaled = Match.value(result.pageStatus).pipe(
|
|
684
|
+
Match.whenOr("SplitRecommended", "SplitRequired", () => true),
|
|
685
|
+
Match.whenOr(null, undefined, () => false),
|
|
686
|
+
Match.exhaustive,
|
|
687
|
+
);
|
|
688
|
+
const shouldSplit =
|
|
689
|
+
typeof splitCursor === "string" &&
|
|
690
|
+
(splitSignaled || result.page.length > 2 * state.options.initialNumItems);
|
|
691
|
+
const shouldRetreat =
|
|
692
|
+
result.page.length === 0 && result.isDone && state.prevStack.length > 0;
|
|
693
|
+
|
|
694
|
+
return Match.value(shouldSplit).pipe(
|
|
695
|
+
Match.withReturnType<Active<Item_, UserArgs_, Error_>>(),
|
|
696
|
+
Match.when(true, () => {
|
|
697
|
+
const deliveredPage = pageFromResult(state, result);
|
|
698
|
+
const previous = Match.value(result.pageStatus).pipe(
|
|
699
|
+
Match.withReturnType<Option.Option<Page<Item_>>>(),
|
|
700
|
+
Match.when("SplitRequired", () => phaseData(state.phase)),
|
|
701
|
+
Match.whenOr("SplitRecommended", null, undefined, () =>
|
|
702
|
+
Option.some(deliveredPage),
|
|
703
|
+
),
|
|
704
|
+
Match.exhaustive,
|
|
705
|
+
);
|
|
706
|
+
return {
|
|
707
|
+
...state,
|
|
708
|
+
requestId: state.requestId + 1,
|
|
709
|
+
current: {
|
|
710
|
+
cursor: state.current.cursor,
|
|
711
|
+
endCursor: Option.fromNullishOr(splitCursor),
|
|
712
|
+
},
|
|
713
|
+
phase: pendingPhase(previous, "Split"),
|
|
714
|
+
};
|
|
715
|
+
}),
|
|
716
|
+
Match.when(false, () =>
|
|
717
|
+
Match.value(shouldRetreat).pipe(
|
|
718
|
+
Match.withReturnType<Active<Item_, UserArgs_, Error_>>(),
|
|
719
|
+
Match.when(true, () => ({
|
|
720
|
+
...state,
|
|
721
|
+
requestId: state.requestId + 1,
|
|
722
|
+
current: state.prevStack[state.prevStack.length - 1],
|
|
723
|
+
prevStack: state.prevStack.slice(0, -1),
|
|
724
|
+
phase: pendingPhase(phaseData(state.phase), "Previous"),
|
|
725
|
+
})),
|
|
726
|
+
Match.when(false, () => ({
|
|
727
|
+
...state,
|
|
728
|
+
phase: Phase.Success<Item_, Error_>({
|
|
729
|
+
data: pageFromResult(state, result),
|
|
730
|
+
}),
|
|
731
|
+
})),
|
|
732
|
+
Match.exhaustive,
|
|
733
|
+
),
|
|
734
|
+
),
|
|
735
|
+
Match.exhaustive,
|
|
736
|
+
);
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const settleFailure = <Item_, UserArgs_, Error_>(
|
|
740
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
741
|
+
error: Error_,
|
|
742
|
+
): Active<Item_, UserArgs_, Error_> => ({
|
|
743
|
+
...state,
|
|
744
|
+
phase: Option.match(phaseData(state.phase), {
|
|
745
|
+
onNone: () => Phase.Failure<Item_, Error_>({ error }),
|
|
746
|
+
onSome: (data) => Phase.Stale<Item_, Error_>({ error, data }),
|
|
747
|
+
}),
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Fold a correlated `Result` into the machine. Like `AsyncData.settle`, a
|
|
752
|
+
* success becomes `Success`; a failure becomes `Stale` when a page is held,
|
|
753
|
+
* otherwise `Failure`. Outcomes from superseded requests and outcomes that
|
|
754
|
+
* arrive after the subscription closed are ignored.
|
|
755
|
+
*/
|
|
756
|
+
export const settle: {
|
|
757
|
+
<Item_, UserArgs_, Error_>(
|
|
758
|
+
settlement: Settlement<Item_, UserArgs_, Error_>,
|
|
759
|
+
): (
|
|
760
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
761
|
+
) => State<Item_, UserArgs_, Error_>;
|
|
762
|
+
<Item_, UserArgs_, Error_>(
|
|
763
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
764
|
+
settlement: Settlement<Item_, UserArgs_, Error_>,
|
|
765
|
+
): State<Item_, UserArgs_, Error_>;
|
|
766
|
+
} = Function.dual(
|
|
767
|
+
2,
|
|
768
|
+
<Item_, UserArgs_, Error_>(
|
|
769
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
770
|
+
settlement: Settlement<Item_, UserArgs_, Error_>,
|
|
771
|
+
): State<Item_, UserArgs_, Error_> =>
|
|
772
|
+
Match.value(state).pipe(
|
|
773
|
+
Match.withReturnType<State<Item_, UserArgs_, Error_>>(),
|
|
774
|
+
Match.tag("Idle", () => state),
|
|
775
|
+
Match.tag("Active", (active) =>
|
|
776
|
+
Match.value(isCurrentRequest(active, settlement.request)).pipe(
|
|
777
|
+
Match.when(false, () => active),
|
|
778
|
+
Match.when(true, () => {
|
|
779
|
+
const allocated = {
|
|
780
|
+
...active,
|
|
781
|
+
paginationId: Option.some(settlement.request.paginationId),
|
|
782
|
+
};
|
|
783
|
+
return Result.match(settlement.result, {
|
|
784
|
+
onSuccess: (result) => settleSuccess(allocated, result),
|
|
785
|
+
onFailure: (error) =>
|
|
786
|
+
Match.value(error).pipe(
|
|
787
|
+
Match.when(
|
|
788
|
+
Match.instanceOf(PaginationError.InvalidCursor),
|
|
789
|
+
() => reset(allocated),
|
|
790
|
+
),
|
|
791
|
+
Match.orElse((other) => settleFailure(allocated, other)),
|
|
792
|
+
),
|
|
793
|
+
});
|
|
794
|
+
}),
|
|
795
|
+
Match.exhaustive,
|
|
796
|
+
),
|
|
797
|
+
),
|
|
798
|
+
Match.exhaustive,
|
|
799
|
+
),
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
/** The complete page currently available to render. */
|
|
803
|
+
export const getPage = <Item_, UserArgs_, Error_>(
|
|
804
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
805
|
+
): Option.Option<Page<Item_>> =>
|
|
806
|
+
Match.value(state).pipe(
|
|
807
|
+
Match.tag("Idle", () => Option.none()),
|
|
808
|
+
Match.tag("Active", ({ phase }) => phaseData(phase)),
|
|
809
|
+
Match.exhaustive,
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
/** The items currently available to render. */
|
|
813
|
+
export const getItems = <Item_, UserArgs_, Error_>(
|
|
814
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
815
|
+
): Option.Option<ReadonlyArray<Item_>> =>
|
|
816
|
+
Option.map(getPage(state), (page) => page.items);
|
|
817
|
+
|
|
818
|
+
/** The error from the most recent failed request, if any. */
|
|
819
|
+
export const getError = <Item_, UserArgs_, Error_>(
|
|
820
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
821
|
+
): Option.Option<Error_> =>
|
|
822
|
+
Match.value(state).pipe(
|
|
823
|
+
Match.withReturnType<Option.Option<Error_>>(),
|
|
824
|
+
Match.tag("Idle", () => Option.none()),
|
|
825
|
+
Match.tag("Active", ({ phase }) =>
|
|
826
|
+
Match.value(phase).pipe(
|
|
827
|
+
Match.withReturnType<Option.Option<Error_>>(),
|
|
828
|
+
Match.tag("Failure", "Stale", ({ error }) => Option.some(error)),
|
|
829
|
+
Match.tag("Loading", "Refreshing", "Success", () => Option.none()),
|
|
830
|
+
Match.exhaustive,
|
|
831
|
+
),
|
|
832
|
+
),
|
|
833
|
+
Match.exhaustive,
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
/** The 1-indexed page number currently targeted by the subscription. */
|
|
837
|
+
export const targetPageNumber = <Item_, UserArgs_, Error_>(
|
|
838
|
+
state: Active<Item_, UserArgs_, Error_>,
|
|
839
|
+
): number => state.prevStack.length + 1;
|
|
840
|
+
|
|
841
|
+
export const hasPage = <Item_, UserArgs_, Error_>(
|
|
842
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
843
|
+
): boolean => Option.isSome(getPage(state));
|
|
844
|
+
|
|
845
|
+
export const hasError = <Item_, UserArgs_, Error_>(
|
|
846
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
847
|
+
): boolean => Option.isSome(getError(state));
|
|
848
|
+
|
|
849
|
+
export const isFirst = <Item_, UserArgs_, Error_>(
|
|
850
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
851
|
+
): boolean =>
|
|
852
|
+
Match.value(state).pipe(
|
|
853
|
+
Match.tag("Idle", () => false),
|
|
854
|
+
Match.tag("Active", ({ prevStack }) => prevStack.length === 0),
|
|
855
|
+
Match.exhaustive,
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
export const isLast = <Item_, UserArgs_, Error_>(
|
|
859
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
860
|
+
): boolean =>
|
|
861
|
+
Match.value(state).pipe(
|
|
862
|
+
Match.tag("Idle", () => false),
|
|
863
|
+
Match.tag("Active", ({ phase }) =>
|
|
864
|
+
Match.value(phase).pipe(
|
|
865
|
+
Match.tag("Success", ({ data }) => data.isDone),
|
|
866
|
+
Match.tag("Loading", "Refreshing", "Failure", "Stale", () => false),
|
|
867
|
+
Match.exhaustive,
|
|
868
|
+
),
|
|
869
|
+
),
|
|
870
|
+
Match.exhaustive,
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
type WithPhase<State_, Phase_> = Omit<State_, "phase"> & {
|
|
874
|
+
readonly phase: Phase_;
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
export const isIdle = <Item_, UserArgs_, Error_>(
|
|
878
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
879
|
+
): state is Idle => state._tag === "Idle";
|
|
880
|
+
|
|
881
|
+
export const isLoading = <Item_, UserArgs_, Error_>(
|
|
882
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
883
|
+
): state is WithPhase<Active<Item_, UserArgs_, Error_>, Loading> =>
|
|
884
|
+
state._tag === "Active" && state.phase._tag === "Loading";
|
|
885
|
+
|
|
886
|
+
export const isRefreshing = <Item_, UserArgs_, Error_>(
|
|
887
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
888
|
+
): state is WithPhase<Active<Item_, UserArgs_, Error_>, Refreshing<Item_>> =>
|
|
889
|
+
state._tag === "Active" && state.phase._tag === "Refreshing";
|
|
890
|
+
|
|
891
|
+
export const isSuccess = <Item_, UserArgs_, Error_>(
|
|
892
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
893
|
+
): state is WithPhase<Active<Item_, UserArgs_, Error_>, Success<Item_>> =>
|
|
894
|
+
state._tag === "Active" && state.phase._tag === "Success";
|
|
895
|
+
|
|
896
|
+
export const isFailure = <Item_, UserArgs_, Error_>(
|
|
897
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
898
|
+
): state is WithPhase<Active<Item_, UserArgs_, Error_>, Failure<Error_>> =>
|
|
899
|
+
state._tag === "Active" && state.phase._tag === "Failure";
|
|
900
|
+
|
|
901
|
+
export const isStale = <Item_, UserArgs_, Error_>(
|
|
902
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
903
|
+
): state is WithPhase<Active<Item_, UserArgs_, Error_>, Stale<Item_, Error_>> =>
|
|
904
|
+
state._tag === "Active" && state.phase._tag === "Stale";
|
|
905
|
+
|
|
906
|
+
export const isPending = <Item_, UserArgs_, Error_>(
|
|
907
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
908
|
+
): boolean =>
|
|
909
|
+
Match.value(state).pipe(
|
|
910
|
+
Match.tag("Idle", () => false),
|
|
911
|
+
Match.tag("Active", ({ phase }) =>
|
|
912
|
+
Match.value(phase).pipe(
|
|
913
|
+
Match.tag("Loading", "Refreshing", () => true),
|
|
914
|
+
Match.tag("Success", "Failure", "Stale", () => false),
|
|
915
|
+
Match.exhaustive,
|
|
916
|
+
),
|
|
917
|
+
),
|
|
918
|
+
Match.exhaustive,
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
export const canNext = <Item_, UserArgs_, Error_>(
|
|
922
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
923
|
+
): boolean =>
|
|
924
|
+
Match.value(state).pipe(
|
|
925
|
+
Match.tag("Idle", () => false),
|
|
926
|
+
Match.tag("Active", ({ phase }) =>
|
|
927
|
+
Match.value(phase).pipe(
|
|
928
|
+
Match.tag("Success", ({ data }) => !data.isDone),
|
|
929
|
+
Match.tag("Loading", "Refreshing", "Failure", "Stale", () => false),
|
|
930
|
+
Match.exhaustive,
|
|
931
|
+
),
|
|
932
|
+
),
|
|
933
|
+
Match.exhaustive,
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
export const canPrev = <Item_, UserArgs_, Error_>(
|
|
937
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
938
|
+
): boolean =>
|
|
939
|
+
Match.value(state).pipe(
|
|
940
|
+
Match.tag("Idle", () => false),
|
|
941
|
+
Match.tag("Active", ({ phase, prevStack }) =>
|
|
942
|
+
Match.value(phase).pipe(
|
|
943
|
+
Match.tag("Success", () => prevStack.length > 0),
|
|
944
|
+
Match.tag("Loading", "Refreshing", "Failure", "Stale", () => false),
|
|
945
|
+
Match.exhaustive,
|
|
946
|
+
),
|
|
947
|
+
),
|
|
948
|
+
Match.exhaustive,
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
/** Pattern-match exhaustively on the machine's AsyncData-style state. */
|
|
952
|
+
export const match: {
|
|
953
|
+
<Item_, Error_, A, B, C, D, E, F>(handlers: {
|
|
954
|
+
readonly onIdle: (idle: Idle) => A;
|
|
955
|
+
readonly onLoading: (loading: Loading) => B;
|
|
956
|
+
readonly onRefreshing: (refreshing: Refreshing<Item_>) => C;
|
|
957
|
+
readonly onFailure: (failure: Failure<Error_>) => D;
|
|
958
|
+
readonly onStale: (stale: Stale<Item_, Error_>) => E;
|
|
959
|
+
readonly onSuccess: (success: Success<Item_>) => F;
|
|
960
|
+
}): <UserArgs_>(
|
|
961
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
962
|
+
) => A | B | C | D | E | F;
|
|
963
|
+
<Item_, UserArgs_, Error_, A, B, C, D, E, F>(
|
|
964
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
965
|
+
handlers: {
|
|
966
|
+
readonly onIdle: (idle: Idle) => A;
|
|
967
|
+
readonly onLoading: (loading: Loading) => B;
|
|
968
|
+
readonly onRefreshing: (refreshing: Refreshing<Item_>) => C;
|
|
969
|
+
readonly onFailure: (failure: Failure<Error_>) => D;
|
|
970
|
+
readonly onStale: (stale: Stale<Item_, Error_>) => E;
|
|
971
|
+
readonly onSuccess: (success: Success<Item_>) => F;
|
|
972
|
+
},
|
|
973
|
+
): A | B | C | D | E | F;
|
|
974
|
+
} = Function.dual(
|
|
975
|
+
2,
|
|
976
|
+
<Item_, UserArgs_, Error_, A, B, C, D, E, F>(
|
|
977
|
+
state: State<Item_, UserArgs_, Error_>,
|
|
978
|
+
handlers: {
|
|
979
|
+
readonly onIdle: (idle: Idle) => A;
|
|
980
|
+
readonly onLoading: (loading: Loading) => B;
|
|
981
|
+
readonly onRefreshing: (refreshing: Refreshing<Item_>) => C;
|
|
982
|
+
readonly onFailure: (failure: Failure<Error_>) => D;
|
|
983
|
+
readonly onStale: (stale: Stale<Item_, Error_>) => E;
|
|
984
|
+
readonly onSuccess: (success: Success<Item_>) => F;
|
|
985
|
+
},
|
|
986
|
+
): A | B | C | D | E | F =>
|
|
987
|
+
Match.value(state).pipe(
|
|
988
|
+
Match.tag("Idle", handlers.onIdle),
|
|
989
|
+
Match.tag("Active", ({ phase }) =>
|
|
990
|
+
Match.value(phase).pipe(
|
|
991
|
+
Match.tag("Loading", handlers.onLoading),
|
|
992
|
+
Match.tag("Refreshing", handlers.onRefreshing),
|
|
993
|
+
Match.tag("Failure", handlers.onFailure),
|
|
994
|
+
Match.tag("Stale", handlers.onStale),
|
|
995
|
+
Match.tag("Success", handlers.onSuccess),
|
|
996
|
+
Match.exhaustive,
|
|
997
|
+
),
|
|
998
|
+
),
|
|
999
|
+
Match.exhaustive,
|
|
1000
|
+
) as A | B | C | D | E | F,
|
|
1001
|
+
);
|