@confect/foldkit 10.0.0-next.22 → 10.0.0-next.23
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 +4 -0
- package/dist/Command.d.ts +36 -31
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +17 -13
- package/dist/Command.js.map +1 -1
- package/dist/PaginatedQuery.d.ts +90 -36
- package/dist/PaginatedQuery.d.ts.map +1 -1
- package/dist/PaginatedQuery.js +49 -21
- package/dist/PaginatedQuery.js.map +1 -1
- package/dist/Subscription.d.ts +39 -36
- package/dist/Subscription.d.ts.map +1 -1
- package/dist/Subscription.js +23 -19
- package/dist/Subscription.js.map +1 -1
- package/package.json +6 -4
- package/src/Command.ts +36 -31
- package/src/PaginatedQuery.ts +90 -36
- package/src/Subscription.ts +39 -36
package/dist/Subscription.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Subscription.js","names":["Client.WebSocketClientError","PaginatedQuery.FunctionError","PaginatedQuery.getSubscriptionRequest","Client.Client","PaginatedQuery.allocateRequest"],"sources":["../src/Subscription.ts"],"sourcesContent":["import * as PaginationError from \"@confect/core/PaginationError\";\nimport type * as Ref from \"@confect/core/Ref\";\nimport * as Effect from \"effect/Effect\";\nimport type * as Equivalence from \"effect/Equivalence\";\nimport * as Match from \"effect/Match\";\nimport * as Option from \"effect/Option\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\nimport * as Stream from \"effect/Stream\";\nimport type * as FoldkitSubscription from \"foldkit/subscription\";\nimport * as PaginatedQuery from \"./PaginatedQuery\";\nimport * as Client from \"./Client\";\n\n/**\n * Everything a reactive query subscription against `Query` can fail with: the\n * ref's typed error (if it declares an `error` schema), a transport-level\n * `WebSocketClientError`, or a `SchemaError` from encoding args or decoding\n * returns.\n */\nexport type Error<Query extends Ref.AnyConfectPublicQuery> =\n | Ref.Error<Query>\n | Client.WebSocketClientError\n | Schema.SchemaError;\n\nconst paginatedError = <Query extends Ref.AnyConfectPublicPaginatedQuery>(\n error: Error<Query>,\n): PaginatedQuery.Error<Query> =>\n Match.value(error).pipe(\n Match.when(Schema.isSchemaError, (schemaError) => schemaError),\n Match.when(Match.instanceOf(Client.WebSocketClientError), (clientError) =>\n PaginationError.fromConvexQueryError(clientError.cause).pipe(\n Option.getOrElse(() => clientError),\n ),\n ),\n Match.when(Match.any, (functionError) =>\n PaginationError.fromConvexErrorData(functionError).pipe(\n Option.getOrElse(() =>\n PaginatedQuery.FunctionError({ error: functionError }),\n ),\n ),\n ),\n Match.exhaustive,\n ) as PaginatedQuery.Error<Query>;\n\n/**\n * The dependency record of a `reactiveQuery` entry. `None` means the\n * subscription is closed; a change from one `Some` to another tears the\n * server subscription down and reopens it with the new args.\n */\nexport interface Dependencies<Query extends Ref.AnyConfectPublicQuery> {\n readonly args: Option.Option<Ref.Args<Query>>;\n}\n\n/**\n * Maps a reactive query's emissions into the app's Messages. Every failure—the ref's typed error, a transport error, or a codec error—arrives via\n * `onError`, so the resulting stream's error channel is `never`, as Foldkit\n * requires.\n */\nexport interface Handlers<\n Query extends Ref.AnyConfectPublicQuery,\n SuccessMessage,\n ErrorMessage,\n> {\n readonly onSuccess: (returns: Ref.Returns<Query>) => SuccessMessage;\n readonly onError: (error: Error<Query>) => ErrorMessage;\n}\n\n/**\n * The `args` extractor is required whenever the query declares args; queries\n * without args may omit it, in which case the subscription is always open.\n */\ntype ArgsConfig<Query extends Ref.AnyConfectPublicQuery, Model> = {\n readonly args?: (model: Model) => Option.Option<Ref.Args<Query>>;\n} & (keyof Ref.Args<Query> extends never\n ? unknown\n : {\n readonly args: (model: Model) => Option.Option<Ref.Args<Query>>;\n });\n\n/**\n * A `dependenciesToStream` body for a hand-written subscription entry: runs\n * the ref as a reactive query against the `Client` resource and maps\n * every emission and every failure into a Message. Query failures are values,\n * so the subscription remains live and can later emit a successful result.\n *\n * Prefer `reactiveQuery`, which builds the whole entry; reach for this when\n * you need control over the entry's dependencies (custom gating, extra\n * dependencies).\n */\nexport const reactiveQueryStream =\n <Query extends Ref.AnyConfectPublicQuery, SuccessMessage, ErrorMessage>(\n ref: Query,\n handlers: Handlers<Query, SuccessMessage, ErrorMessage>,\n ) =>\n (\n ...args: Ref.OptionalArgs<Query>\n ): Stream.Stream<SuccessMessage | ErrorMessage, never, Client.Client> =>\n Client.Client.pipe(\n Effect.map((client) => client.reactiveQueryResult(ref, ...args)),\n Stream.unwrap,\n Stream.map(\n Result.match({\n onSuccess: handlers.onSuccess,\n onFailure: handlers.onError,\n }),\n ),\n );\n\n/**\n * A complete Foldkit subscription entry for a Confect reactive query. The\n * leading thunk fixes the `Model` type (TypeScript cannot partially infer\n * type arguments), so the `args` extractor's parameter is already typed.\n * Pass the result as an entry value to `Subscription.make`:\n *\n * ```ts\n * import * as Confect from \"@confect/foldkit\"\n * import * as Subscription from \"foldkit/subscription\"\n *\n * const subscriptions = Subscription.make<\n * Model,\n * Message,\n * Confect.Client.Client\n * >()(() => ({\n * note: Confect.Subscription.reactiveQuery<Model>()(refs.public.notes.get, {\n * args: (model) => Option.map(model.noteId, (noteId) => ({ noteId })),\n * onSuccess: (note) => SucceededGetNote({ note }),\n * onError: (error) => FailedGetNote({ message: String(error) }),\n * }),\n * }))\n * ```\n *\n * The entry's dependencies are the query args wrapped in `Option`: `None`\n * closes the subscription, a change from one `Some` to another unsubscribes\n * and resubscribes with the new args, and structurally equal args leave the\n * subscription running (equivalence is derived from the ref's args schema).\n * Queries without args may omit `args`, leaving the subscription always open.\n */\nexport const reactiveQuery =\n <Model>() =>\n <Query extends Ref.AnyConfectPublicQuery, SuccessMessage, ErrorMessage>(\n ref: Query,\n config: Handlers<Query, SuccessMessage, ErrorMessage> &\n ArgsConfig<Query, Model>,\n ): FoldkitSubscription.EntryWithoutKeepAlive<\n Model,\n SuccessMessage | ErrorMessage,\n Dependencies<Query>,\n Client.Client\n > => {\n const modelToArgs = config.args;\n\n return {\n dependenciesSchema: Schema.Struct({\n args: Schema.Option(ref.args),\n }),\n modelToDependencies: (model) => ({\n args:\n modelToArgs === undefined\n ? Option.some({} as Ref.Args<Query>)\n : modelToArgs(model),\n }),\n dependenciesToStream: ({ args }) =>\n Option.match(args, {\n onNone: () => Stream.empty,\n onSome: (someArgs) => reactiveQueryStream(ref, config)(someArgs),\n }),\n };\n };\n\n/**\n * A complete Foldkit subscription entry that keeps exactly one live reactive\n * page subscription in sync with a `PaginatedQuery` machine in the Model.\n * The machine state is the single source of truth: navigation and\n * split-pinning change the derived args, while `Idle` closes the subscription.\n * Pagination ids are allocated here and installed by the first settlement.\n *\n * The leading thunk fixes the `Model` type (TypeScript cannot partially\n * infer type arguments), so the `state` extractor's parameter is already\n * typed:\n *\n * ```ts\n * import * as Confect from \"@confect/foldkit\"\n * import * as Subscription from \"foldkit/subscription\"\n *\n * const subscriptions = Subscription.make<\n * Model,\n * Message,\n * Confect.Client.Client\n * >()(() => ({\n * notesPage: Confect.Subscription.paginatedQuery<Model>()(Notes, {\n * state: (model) => model.notes,\n * onSettled: (settlement) => SettledGetNotesPage({ settlement }),\n * }),\n * }))\n * ```\n *\n * Pass the settlement to `PaginatedQuery.settle` in `update`. The request\n * identity is included in both success and failure Messages, so superseded\n * outcomes cannot settle a newer page or pagination session. Query failures\n * do not close the Convex subscription; a later result can recover naturally.\n */\ntype DependenciesSchema<Dependencies_> = Schema.Schema<Dependencies_> & {\n readonly fields: Schema.Struct.Fields;\n};\n\ntype EntryWithKeepAlive<Model, Message, Dependencies_, Services> = {\n readonly dependenciesSchema: DependenciesSchema<Dependencies_>;\n readonly modelToDependencies: (model: Model) => Dependencies_;\n readonly keepAliveEquivalence: Equivalence.Equivalence<Dependencies_>;\n readonly dependenciesToStream: (\n dependencies: Dependencies_,\n readDependencies: () => Dependencies_,\n ) => Stream.Stream<Message, never, Services>;\n};\n\nexport const paginatedQuery =\n <Model>() =>\n <\n PaginatedQueryRef extends Ref.AnyConfectPublicPaginatedQuery,\n SettledMessage,\n >(\n machine: PaginatedQuery.PaginatedQuery<PaginatedQueryRef>,\n config: {\n readonly state: (\n model: Model,\n ) => PaginatedQuery.State<\n PaginatedQuery.Item<PaginatedQueryRef>,\n PaginatedQuery.UserArgs<PaginatedQueryRef>,\n PaginatedQuery.Error<PaginatedQueryRef>\n >;\n readonly onSettled: (\n settlement: PaginatedQuery.Settlement<\n PaginatedQuery.Item<PaginatedQueryRef>,\n PaginatedQuery.UserArgs<PaginatedQueryRef>,\n PaginatedQuery.Error<PaginatedQueryRef>\n >,\n ) => SettledMessage;\n },\n ): EntryWithKeepAlive<\n Model,\n SettledMessage,\n {\n readonly request: Option.Option<\n PaginatedQuery.SubscriptionRequest<\n PaginatedQuery.UserArgs<PaginatedQueryRef>\n >\n >;\n },\n Client.Client\n > => {\n const ref = machine.ref;\n const composedArgsSchema = ref.args;\n const requestEquivalence = Schema.toEquivalence(\n machine.subscriptionRequestSchema,\n );\n const optionalRequestEquivalence = Option.makeEquivalence<\n PaginatedQuery.SubscriptionRequest<\n PaginatedQuery.UserArgs<PaginatedQueryRef>\n >\n >((left, right) =>\n requestEquivalence(\n { ...left, paginationId: Option.none() },\n { ...right, paginationId: Option.none() },\n ),\n );\n\n return {\n dependenciesSchema: Schema.Struct({\n request: Schema.Option(machine.subscriptionRequestSchema),\n }),\n modelToDependencies: (model) => ({\n request: Match.value(config.state(model)).pipe(\n Match.tag(\"Idle\", () => Option.none()),\n Match.tag(\"Active\", (state) =>\n Option.some(PaginatedQuery.getSubscriptionRequest(state)),\n ),\n Match.exhaustive,\n ),\n }),\n keepAliveEquivalence: (left, right) =>\n optionalRequestEquivalence(left.request, right.request),\n dependenciesToStream: ({\n request,\n }): Stream.Stream<SettledMessage, never, Client.Client> =>\n Option.match(request, {\n onNone: () => Stream.empty,\n onSome: (subscriptionRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* Client.Client;\n const paginationId = yield* client.resolvePaginationId(\n subscriptionRequest.paginationId,\n );\n const allocatedRequest = PaginatedQuery.allocateRequest(\n subscriptionRequest,\n paginationId,\n );\n const { descriptor, options } = allocatedRequest;\n const composedArgs = composedArgsSchema.make({\n ...allocatedRequest.args,\n paginationOpts: {\n numItems: options.initialNumItems,\n cursor: descriptor.cursor,\n id: paginationId,\n ...Match.value(options.maximumRowsRead).pipe(\n Match.when(undefined, () => ({})),\n Match.when(Match.defined, (maximumRowsRead) => ({\n maximumRowsRead,\n })),\n Match.exhaustive,\n ),\n ...Match.value(options.maximumBytesRead).pipe(\n Match.when(undefined, () => ({})),\n Match.when(Match.defined, (maximumBytesRead) => ({\n maximumBytesRead,\n })),\n Match.exhaustive,\n ),\n ...Option.match(descriptor.endCursor, {\n onNone: () => ({}),\n onSome: (endCursor) => ({ endCursor }),\n }),\n },\n });\n return client.reactiveQueryResult(ref, composedArgs).pipe(\n Stream.map((result) =>\n config.onSettled({\n request: allocatedRequest,\n result: Result.mapError(result, paginatedError),\n }),\n ),\n );\n }),\n ),\n }),\n };\n };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Subscription.js","names":["Client.WebSocketClientError","PaginatedQuery.FunctionError","PaginatedQuery.getSubscriptionRequest","Client.Client","PaginatedQuery.allocateRequest"],"sources":["../src/Subscription.ts"],"sourcesContent":["import * as PaginationError from \"@confect/core/PaginationError\";\nimport type * as Ref from \"@confect/core/Ref\";\nimport * as Effect from \"effect/Effect\";\nimport type * as Equivalence from \"effect/Equivalence\";\nimport * as Match from \"effect/Match\";\nimport * as Option from \"effect/Option\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\nimport * as Stream from \"effect/Stream\";\nimport type * as FoldkitSubscription from \"foldkit/subscription\";\nimport * as PaginatedQuery from \"./PaginatedQuery\";\nimport * as Client from \"./Client\";\n\n/**\n * Everything a reactive query subscription against `Query` can fail with: the\n * ref's typed error (if it declares an `error` schema), a transport-level\n * `WebSocketClientError`, or a `SchemaError` from encoding args or decoding\n * returns.\n */\nexport type Error<Query extends Ref.AnyConfectPublicQuery> =\n | Ref.Error<Query>\n | Client.WebSocketClientError\n | Schema.SchemaError;\n\nconst paginatedError = <Query extends Ref.AnyConfectPublicPaginatedQuery>(\n error: Error<Query>,\n): PaginatedQuery.Error<Query> =>\n Match.value(error).pipe(\n Match.when(Schema.isSchemaError, (schemaError) => schemaError),\n Match.when(Match.instanceOf(Client.WebSocketClientError), (clientError) =>\n PaginationError.fromConvexQueryError(clientError.cause).pipe(\n Option.getOrElse(() => clientError),\n ),\n ),\n Match.when(Match.any, (functionError) =>\n PaginationError.fromConvexErrorData(functionError).pipe(\n Option.getOrElse(() =>\n PaginatedQuery.FunctionError({ error: functionError }),\n ),\n ),\n ),\n Match.exhaustive,\n ) as PaginatedQuery.Error<Query>;\n\n/**\n * The dependency record of a `reactiveQuery` entry. `None` means the\n * subscription is closed; a change from one `Some` to another tears the server\n * subscription down and reopens it with the new args.\n */\nexport interface Dependencies<Query extends Ref.AnyConfectPublicQuery> {\n readonly args: Option.Option<Ref.Args<Query>>;\n}\n\n/**\n * Maps a reactive query's emissions into the app's Messages. Every failure—the\n * ref's typed error, a transport error, or a codec error—arrives via `onError`,\n * so the resulting stream's error channel is `never`, as Foldkit requires.\n */\nexport interface Handlers<\n Query extends Ref.AnyConfectPublicQuery,\n SuccessMessage,\n ErrorMessage,\n> {\n readonly onSuccess: (returns: Ref.Returns<Query>) => SuccessMessage;\n readonly onError: (error: Error<Query>) => ErrorMessage;\n}\n\n/**\n * The `args` extractor is required whenever the query declares args; queries\n * without args may omit it, in which case the subscription is always open.\n */\ntype ArgsConfig<Query extends Ref.AnyConfectPublicQuery, Model> = {\n readonly args?: (model: Model) => Option.Option<Ref.Args<Query>>;\n} & (keyof Ref.Args<Query> extends never\n ? unknown\n : {\n readonly args: (model: Model) => Option.Option<Ref.Args<Query>>;\n });\n\n/**\n * A `dependenciesToStream` body for a hand-written subscription entry: runs the\n * ref as a reactive query against the `Client` resource and maps every emission\n * and every failure into a Message. Query failures are values, so the\n * subscription remains live and can later emit a successful result.\n *\n * Prefer `reactiveQuery`, which builds the whole entry; reach for this when you\n * need control over the entry's dependencies (custom gating, extra\n * dependencies).\n */\nexport const reactiveQueryStream =\n <Query extends Ref.AnyConfectPublicQuery, SuccessMessage, ErrorMessage>(\n ref: Query,\n handlers: Handlers<Query, SuccessMessage, ErrorMessage>,\n ) =>\n (\n ...args: Ref.OptionalArgs<Query>\n ): Stream.Stream<SuccessMessage | ErrorMessage, never, Client.Client> =>\n Client.Client.pipe(\n Effect.map((client) => client.reactiveQueryResult(ref, ...args)),\n Stream.unwrap,\n Stream.map(\n Result.match({\n onSuccess: handlers.onSuccess,\n onFailure: handlers.onError,\n }),\n ),\n );\n\n/**\n * A complete Foldkit subscription entry for a Confect reactive query. The\n * leading thunk fixes the `Model` type (TypeScript cannot partially infer type\n * arguments), so the `args` extractor's parameter is already typed. Pass the\n * result as an entry value to `Subscription.make`:\n *\n * ```ts\n * import * as Confect from \"@confect/foldkit\";\n * import * as Subscription from \"foldkit/subscription\";\n *\n * const subscriptions = Subscription.make<\n * Model,\n * Message,\n * Confect.Client.Client\n * >()(() => ({\n * note: Confect.Subscription.reactiveQuery<Model>()(\n * refs.public.notes.get,\n * {\n * args: (model) =>\n * Option.map(model.noteId, (noteId) => ({ noteId })),\n * onSuccess: (note) => SucceededGetNote({ note }),\n * onError: (error) => FailedGetNote({ message: String(error) }),\n * },\n * ),\n * }));\n * ```\n *\n * The entry's dependencies are the query args wrapped in `Option`: `None`\n * closes the subscription, a change from one `Some` to another unsubscribes and\n * resubscribes with the new args, and structurally equal args leave the\n * subscription running (equivalence is derived from the ref's args schema).\n * Queries without args may omit `args`, leaving the subscription always open.\n */\nexport const reactiveQuery =\n <Model>() =>\n <Query extends Ref.AnyConfectPublicQuery, SuccessMessage, ErrorMessage>(\n ref: Query,\n config: Handlers<Query, SuccessMessage, ErrorMessage> &\n ArgsConfig<Query, Model>,\n ): FoldkitSubscription.EntryWithoutKeepAlive<\n Model,\n SuccessMessage | ErrorMessage,\n Dependencies<Query>,\n Client.Client\n > => {\n const modelToArgs = config.args;\n\n return {\n dependenciesSchema: Schema.Struct({\n args: Schema.Option(ref.args),\n }),\n modelToDependencies: (model) => ({\n args:\n modelToArgs === undefined\n ? Option.some({} as Ref.Args<Query>)\n : modelToArgs(model),\n }),\n dependenciesToStream: ({ args }) =>\n Option.match(args, {\n onNone: () => Stream.empty,\n onSome: (someArgs) => reactiveQueryStream(ref, config)(someArgs),\n }),\n };\n };\n\n/**\n * A complete Foldkit subscription entry that keeps exactly one live reactive\n * page subscription in sync with a `PaginatedQuery` machine in the Model. The\n * machine state is the single source of truth: navigation and split-pinning\n * change the derived args, while `Idle` closes the subscription. Pagination ids\n * are allocated here and installed by the first settlement.\n *\n * The leading thunk fixes the `Model` type (TypeScript cannot partially infer\n * type arguments), so the `state` extractor's parameter is already typed:\n *\n * ```ts\n * import * as Confect from \"@confect/foldkit\";\n * import * as Subscription from \"foldkit/subscription\";\n *\n * const subscriptions = Subscription.make<\n * Model,\n * Message,\n * Confect.Client.Client\n * >()(() => ({\n * notesPage: Confect.Subscription.paginatedQuery<Model>()(Notes, {\n * state: (model) => model.notes,\n * onSettled: (settlement) => SettledGetNotesPage({ settlement }),\n * }),\n * }));\n * ```\n *\n * Pass the settlement to `PaginatedQuery.settle` in `update`. The request\n * identity is included in both success and failure Messages, so superseded\n * outcomes cannot settle a newer page or pagination session. Query failures do\n * not close the Convex subscription; a later result can recover naturally.\n */\ntype DependenciesSchema<Dependencies_> = Schema.Schema<Dependencies_> & {\n readonly fields: Schema.Struct.Fields;\n};\n\ntype EntryWithKeepAlive<Model, Message, Dependencies_, Services> = {\n readonly dependenciesSchema: DependenciesSchema<Dependencies_>;\n readonly modelToDependencies: (model: Model) => Dependencies_;\n readonly keepAliveEquivalence: Equivalence.Equivalence<Dependencies_>;\n readonly dependenciesToStream: (\n dependencies: Dependencies_,\n readDependencies: () => Dependencies_,\n ) => Stream.Stream<Message, never, Services>;\n};\n\nexport const paginatedQuery =\n <Model>() =>\n <\n PaginatedQueryRef extends Ref.AnyConfectPublicPaginatedQuery,\n SettledMessage,\n >(\n machine: PaginatedQuery.PaginatedQuery<PaginatedQueryRef>,\n config: {\n readonly state: (\n model: Model,\n ) => PaginatedQuery.State<\n PaginatedQuery.Item<PaginatedQueryRef>,\n PaginatedQuery.UserArgs<PaginatedQueryRef>,\n PaginatedQuery.Error<PaginatedQueryRef>\n >;\n readonly onSettled: (\n settlement: PaginatedQuery.Settlement<\n PaginatedQuery.Item<PaginatedQueryRef>,\n PaginatedQuery.UserArgs<PaginatedQueryRef>,\n PaginatedQuery.Error<PaginatedQueryRef>\n >,\n ) => SettledMessage;\n },\n ): EntryWithKeepAlive<\n Model,\n SettledMessage,\n {\n readonly request: Option.Option<\n PaginatedQuery.SubscriptionRequest<\n PaginatedQuery.UserArgs<PaginatedQueryRef>\n >\n >;\n },\n Client.Client\n > => {\n const ref = machine.ref;\n const composedArgsSchema = ref.args;\n const requestEquivalence = Schema.toEquivalence(\n machine.subscriptionRequestSchema,\n );\n const optionalRequestEquivalence = Option.makeEquivalence<\n PaginatedQuery.SubscriptionRequest<\n PaginatedQuery.UserArgs<PaginatedQueryRef>\n >\n >((left, right) =>\n requestEquivalence(\n { ...left, paginationId: Option.none() },\n { ...right, paginationId: Option.none() },\n ),\n );\n\n return {\n dependenciesSchema: Schema.Struct({\n request: Schema.Option(machine.subscriptionRequestSchema),\n }),\n modelToDependencies: (model) => ({\n request: Match.value(config.state(model)).pipe(\n Match.tag(\"Idle\", () => Option.none()),\n Match.tag(\"Active\", (state) =>\n Option.some(PaginatedQuery.getSubscriptionRequest(state)),\n ),\n Match.exhaustive,\n ),\n }),\n keepAliveEquivalence: (left, right) =>\n optionalRequestEquivalence(left.request, right.request),\n dependenciesToStream: ({\n request,\n }): Stream.Stream<SettledMessage, never, Client.Client> =>\n Option.match(request, {\n onNone: () => Stream.empty,\n onSome: (subscriptionRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* Client.Client;\n const paginationId = yield* client.resolvePaginationId(\n subscriptionRequest.paginationId,\n );\n const allocatedRequest = PaginatedQuery.allocateRequest(\n subscriptionRequest,\n paginationId,\n );\n const { descriptor, options } = allocatedRequest;\n const composedArgs = composedArgsSchema.make({\n ...allocatedRequest.args,\n paginationOpts: {\n numItems: options.initialNumItems,\n cursor: descriptor.cursor,\n id: paginationId,\n ...Match.value(options.maximumRowsRead).pipe(\n Match.when(undefined, () => ({})),\n Match.when(Match.defined, (maximumRowsRead) => ({\n maximumRowsRead,\n })),\n Match.exhaustive,\n ),\n ...Match.value(options.maximumBytesRead).pipe(\n Match.when(undefined, () => ({})),\n Match.when(Match.defined, (maximumBytesRead) => ({\n maximumBytesRead,\n })),\n Match.exhaustive,\n ),\n ...Option.match(descriptor.endCursor, {\n onNone: () => ({}),\n onSome: (endCursor) => ({ endCursor }),\n }),\n },\n });\n return client.reactiveQueryResult(ref, composedArgs).pipe(\n Stream.map((result) =>\n config.onSettled({\n request: allocatedRequest,\n result: Result.mapError(result, paginatedError),\n }),\n ),\n );\n }),\n ),\n }),\n };\n };\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@confect/foldkit",
|
|
3
3
|
"description": "Client-side bindings for Foldkit apps",
|
|
4
|
-
"version": "10.0.0-next.
|
|
4
|
+
"version": "10.0.0-next.23",
|
|
5
5
|
"author": "RJ Dellecese",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/rjdellecese/confect/issues"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@effect/vitest": "4.0.0-rc.115",
|
|
11
11
|
"@types/node": "24.13.3",
|
|
12
|
+
"confect-test-types": "0.0.0",
|
|
12
13
|
"convex": "1.45.0",
|
|
13
14
|
"effect": "4.0.0-rc.115",
|
|
14
15
|
"foldkit": "0.148.1",
|
|
@@ -50,8 +51,8 @@
|
|
|
50
51
|
"main": "./dist/index.js",
|
|
51
52
|
"module": "./dist/index.js",
|
|
52
53
|
"peerDependencies": {
|
|
53
|
-
"@confect/core": "^10.0.0-next.
|
|
54
|
-
"@confect/js": "^10.0.0-next.
|
|
54
|
+
"@confect/core": "^10.0.0-next.23",
|
|
55
|
+
"@confect/js": "^10.0.0-next.23",
|
|
55
56
|
"effect": "^4.0.0-rc.115",
|
|
56
57
|
"foldkit": "^0.148.0"
|
|
57
58
|
},
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"clean": "shx rm -rf dist coverage node_modules",
|
|
67
68
|
"dev": "tsdown --watch --no-clean --config-loader unrun",
|
|
68
69
|
"test": "vitest run",
|
|
69
|
-
"typecheck": "tsc -b tsconfig.src.json"
|
|
70
|
+
"typecheck": "tsc -b tsconfig.src.json",
|
|
71
|
+
"typecheck:inexact": "tsc --noEmit --project tsconfig.json --exactOptionalPropertyTypes false"
|
|
70
72
|
}
|
|
71
73
|
}
|
package/src/Command.ts
CHANGED
|
@@ -6,10 +6,10 @@ import * as FoldkitCommand from "foldkit/command";
|
|
|
6
6
|
import * as Client from "./Client";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Everything a Command against `Ref_` can fail with before it is folded into
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Everything a Command against `Ref_` can fail with before it is folded into an
|
|
10
|
+
* `onError` Message: the ref's typed error (if it declares an `error` schema),
|
|
11
|
+
* a transport-level `WebSocketClientError`, or a `SchemaError` from encoding
|
|
12
|
+
* args or decoding returns.
|
|
13
13
|
*/
|
|
14
14
|
export type Error<Ref_ extends Ref.AnyConfect> =
|
|
15
15
|
| Ref.Error<Ref_>
|
|
@@ -17,9 +17,10 @@ export type Error<Ref_ extends Ref.AnyConfect> =
|
|
|
17
17
|
| Schema.SchemaError;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Maps a Confect function's outcome into the app's Messages. Every failure—the
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Maps a Confect function's outcome into the app's Messages. Every failure—the
|
|
21
|
+
* ref's typed error, a transport error, or a codec error—arrives via `onError`,
|
|
22
|
+
* so the resulting Command's error channel is `never`, as Foldkit requires.
|
|
23
|
+
* Follow Foldkit's naming convention: past-tense facts like
|
|
23
24
|
* `SucceededSaveNote`/`FailedSaveNote`.
|
|
24
25
|
*/
|
|
25
26
|
export interface Handlers<
|
|
@@ -49,13 +50,13 @@ type SchemaArgs<Ref_ extends Ref.AnyConfect> = Schema.Schema.Type<
|
|
|
49
50
|
>;
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
* A Foldkit Command definition whose args are the ref's args: our
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
53
|
+
* A Foldkit Command definition whose args are the ref's args: our ref-derived
|
|
54
|
+
* call signature (args optional when the ref declares none) intersected with
|
|
55
|
+
* Foldkit's own `CommandDefinitionWithArgs`, which supplies the
|
|
56
|
+
* `CommandDefinitionTypeId` brand and makes the definition assignable wherever
|
|
57
|
+
* Foldkit accepts one—including Story/Scene `Command.resolve` and `expectExact`
|
|
58
|
+
* matchers. Call it from `update` to construct a Command instance; nothing runs
|
|
59
|
+
* until the Foldkit runtime executes it.
|
|
59
60
|
*/
|
|
60
61
|
export type Definition<
|
|
61
62
|
Name extends string,
|
|
@@ -76,8 +77,8 @@ export type Definition<
|
|
|
76
77
|
* invocations can be targeted independently.
|
|
77
78
|
*
|
|
78
79
|
* Interrupting stops the client-side Effect and guarantees the invocation's
|
|
79
|
-
* result Messages never dispatch—it does not cancel the Convex function on
|
|
80
|
-
*
|
|
80
|
+
* result Messages never dispatch—it does not cancel the Convex function on the
|
|
81
|
+
* server, which runs to completion once the call is on the wire.
|
|
81
82
|
*/
|
|
82
83
|
export type InterruptOption<
|
|
83
84
|
Ref_ extends Ref.AnyConfect,
|
|
@@ -97,8 +98,8 @@ export type KeyedInterrupt<
|
|
|
97
98
|
/**
|
|
98
99
|
* An interruptible Command definition whose key is the Command name—the
|
|
99
100
|
* ref-derived call signature intersected with Foldkit's
|
|
100
|
-
* `Interruptible.DefinitionWithArgsNameKeyed`, which supplies the brand and
|
|
101
|
-
*
|
|
101
|
+
* `Interruptible.DefinitionWithArgsNameKeyed`, which supplies the brand and the
|
|
102
|
+
* `Interrupt` constructor.
|
|
102
103
|
*/
|
|
103
104
|
export type InterruptibleDefinition<
|
|
104
105
|
Name extends string,
|
|
@@ -149,8 +150,8 @@ const run = <Ref_ extends Ref.AnyConfect, SuccessMessage, ErrorMessage>(
|
|
|
149
150
|
|
|
150
151
|
/**
|
|
151
152
|
* An execute body for a hand-written `Command.define`—every failure already
|
|
152
|
-
* folded into a Message. Reach for these when the factory below doesn't fit:
|
|
153
|
-
*
|
|
153
|
+
* folded into a Message. Reach for these when the factory below doesn't fit: a
|
|
154
|
+
* custom args schema, `interrupt`, or a Command that makes several calls.
|
|
154
155
|
*/
|
|
155
156
|
export const queryEffect =
|
|
156
157
|
<Query extends Ref.AnyConfectPublicQuery, SuccessMessage, ErrorMessage>(
|
|
@@ -411,22 +412,26 @@ const makeFactory = <BoundRef extends Ref.AnyConfect>(
|
|
|
411
412
|
* messages: [SucceededGetNote, FailedGetNote],
|
|
412
413
|
* onSuccess: (note) => SucceededGetNote({ note }),
|
|
413
414
|
* onError: (error) => FailedGetNote({ message: String(error) }),
|
|
414
|
-
* })
|
|
415
|
+
* });
|
|
415
416
|
* // In update:
|
|
416
417
|
* // [model, [FetchNote({ noteId })]]
|
|
417
418
|
* ```
|
|
418
419
|
*
|
|
419
420
|
* Pass `interrupt` to make invocations interruptible via the returned
|
|
420
|
-
* definition's `Interrupt` constructor—`true` keys them by the Command
|
|
421
|
-
*
|
|
421
|
+
* definition's `Interrupt` constructor—`true` keys them by the Command name, `{
|
|
422
|
+
* keyFields, toKey }` by a part derived from the ref's args:
|
|
422
423
|
*
|
|
423
424
|
* ```ts
|
|
424
|
-
* const SaveDraft = Command.mutation(
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
* })
|
|
425
|
+
* const SaveDraft = Command.mutation(
|
|
426
|
+
* "SaveDraft",
|
|
427
|
+
* refs.public.notes.insert,
|
|
428
|
+
* {
|
|
429
|
+
* messages: [SucceededSaveDraft, FailedSaveDraft],
|
|
430
|
+
* onSuccess: (noteId) => SucceededSaveDraft({ noteId }),
|
|
431
|
+
* onError: (error) => FailedSaveDraft({ message: String(error) }),
|
|
432
|
+
* interrupt: true,
|
|
433
|
+
* },
|
|
434
|
+
* );
|
|
430
435
|
* // In update:
|
|
431
436
|
* // [model, [SaveDraft.Interrupt((outcome) => CompletedCancelSaveDraft({ outcome }))]]
|
|
432
437
|
* ```
|
|
@@ -443,8 +448,8 @@ export const mutation: ReturnType<
|
|
|
443
448
|
> = makeFactory(mutationEffect);
|
|
444
449
|
|
|
445
450
|
/**
|
|
446
|
-
* A Foldkit Command definition for a Confect action whose Command args are
|
|
447
|
-
*
|
|
451
|
+
* A Foldkit Command definition for a Confect action whose Command args are the
|
|
452
|
+
* ref's args. See `query`.
|
|
448
453
|
*/
|
|
449
454
|
export const action: ReturnType<
|
|
450
455
|
typeof makeFactory<Ref.AnyConfectPublicAction>
|
package/src/PaginatedQuery.ts
CHANGED
|
@@ -11,7 +11,9 @@ import * as SchemaGetter from "effect/SchemaGetter";
|
|
|
11
11
|
import * as SchemaIssue from "effect/SchemaIssue";
|
|
12
12
|
import * as Client from "./Client";
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Identifies the cursor range fetched for one page.
|
|
16
|
+
*/
|
|
15
17
|
export interface PageDescriptor {
|
|
16
18
|
readonly cursor: string | null;
|
|
17
19
|
readonly endCursor: Option.Option<string>;
|
|
@@ -22,7 +24,9 @@ export const PageDescriptor = Schema.Struct({
|
|
|
22
24
|
endCursor: Schema.Option(Schema.String),
|
|
23
25
|
});
|
|
24
26
|
|
|
25
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Options that remain fixed for one pagination session.
|
|
29
|
+
*/
|
|
26
30
|
export interface Options {
|
|
27
31
|
readonly initialNumItems: number;
|
|
28
32
|
readonly maximumRowsRead?: number;
|
|
@@ -38,7 +42,9 @@ export const Options = Schema.Struct({
|
|
|
38
42
|
maximumBytesRead: Schema.optionalKey(PositiveInt),
|
|
39
43
|
});
|
|
40
44
|
|
|
41
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Why the machine is fetching its current target.
|
|
47
|
+
*/
|
|
42
48
|
export type Direction =
|
|
43
49
|
| "Initial"
|
|
44
50
|
| "Next"
|
|
@@ -47,7 +53,9 @@ export type Direction =
|
|
|
47
53
|
| "Split"
|
|
48
54
|
| "Reset";
|
|
49
55
|
|
|
50
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* A complete page that can remain on screen while another page is fetched.
|
|
58
|
+
*/
|
|
51
59
|
export interface Page<Item_> {
|
|
52
60
|
readonly descriptor: PageDescriptor;
|
|
53
61
|
readonly number: number;
|
|
@@ -89,12 +97,16 @@ interface PhaseDefinition extends Data.TaggedEnum.WithGenerics<2> {
|
|
|
89
97
|
|
|
90
98
|
const Phase = Data.taggedEnum<PhaseDefinition>();
|
|
91
99
|
|
|
92
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* A closed machine that retains its generation for stale-result rejection.
|
|
102
|
+
*/
|
|
93
103
|
export type Idle = Data.TaggedEnum<{
|
|
94
104
|
Idle: { readonly generation: number };
|
|
95
105
|
}>;
|
|
96
106
|
|
|
97
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* A live cursor-pagination session.
|
|
109
|
+
*/
|
|
98
110
|
export type Active<Item_, UserArgs_, Error_> = Data.TaggedEnum<{
|
|
99
111
|
Active: {
|
|
100
112
|
readonly generation: number;
|
|
@@ -108,7 +120,9 @@ export type Active<Item_, UserArgs_, Error_> = Data.TaggedEnum<{
|
|
|
108
120
|
};
|
|
109
121
|
}>;
|
|
110
122
|
|
|
111
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* A schema-backed cursor-pagination machine.
|
|
125
|
+
*/
|
|
112
126
|
export type State<Item_, UserArgs_, Error_> =
|
|
113
127
|
| Idle
|
|
114
128
|
| Active<Item_, UserArgs_, Error_>;
|
|
@@ -119,7 +133,9 @@ interface StateDefinition extends Data.TaggedEnum.WithGenerics<3> {
|
|
|
119
133
|
|
|
120
134
|
const State = Data.taggedEnum<StateDefinition>();
|
|
121
135
|
|
|
122
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* The logical request from which a subscription allocates a session id.
|
|
138
|
+
*/
|
|
123
139
|
export interface SubscriptionRequest<UserArgs_> {
|
|
124
140
|
readonly generation: number;
|
|
125
141
|
readonly args: UserArgs_;
|
|
@@ -129,7 +145,9 @@ export interface SubscriptionRequest<UserArgs_> {
|
|
|
129
145
|
readonly descriptor: PageDescriptor;
|
|
130
146
|
}
|
|
131
147
|
|
|
132
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* The complete identity and arguments of a subscribed page request.
|
|
150
|
+
*/
|
|
133
151
|
export interface Request<UserArgs_> {
|
|
134
152
|
readonly generation: number;
|
|
135
153
|
readonly args: UserArgs_;
|
|
@@ -139,7 +157,9 @@ export interface Request<UserArgs_> {
|
|
|
139
157
|
readonly descriptor: PageDescriptor;
|
|
140
158
|
}
|
|
141
159
|
|
|
142
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* The successful payload returned by a Convex paginated query.
|
|
162
|
+
*/
|
|
143
163
|
export interface PageResult<Item_> {
|
|
144
164
|
readonly page: ReadonlyArray<Item_>;
|
|
145
165
|
readonly isDone: boolean;
|
|
@@ -149,8 +169,8 @@ export interface PageResult<Item_> {
|
|
|
149
169
|
}
|
|
150
170
|
|
|
151
171
|
/**
|
|
152
|
-
* One correlated subscription outcome. Both success and failure travel
|
|
153
|
-
*
|
|
172
|
+
* One correlated subscription outcome. Both success and failure travel through
|
|
173
|
+
* this value so `settle` can mirror `AsyncData.settle`.
|
|
154
174
|
*/
|
|
155
175
|
export interface Settlement<Item_, UserArgs_, Error_> {
|
|
156
176
|
readonly request: Request<UserArgs_>;
|
|
@@ -173,18 +193,24 @@ interface FunctionErrorDefinition extends Data.TaggedEnum.WithGenerics<1> {
|
|
|
173
193
|
readonly taggedEnum: TaggedFunctionError<this["A"]>;
|
|
174
194
|
}
|
|
175
195
|
|
|
176
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Constructs a declared function or middleware failure envelope.
|
|
198
|
+
*/
|
|
177
199
|
export const FunctionError =
|
|
178
200
|
Data.taggedEnum<FunctionErrorDefinition>().FunctionError;
|
|
179
201
|
|
|
180
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* A declared function or middleware failure, kept distinct from client errors.
|
|
204
|
+
*/
|
|
181
205
|
export type FunctionError<Query extends Ref.AnyConfectPublicPaginatedQuery> = [
|
|
182
206
|
Ref.Error<Query>,
|
|
183
207
|
] extends [never]
|
|
184
208
|
? never
|
|
185
209
|
: TaggedFunctionError<Ref.Error<Query>>;
|
|
186
210
|
|
|
187
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Every failure a paginated query subscription can settle with.
|
|
213
|
+
*/
|
|
188
214
|
export type Error<Query extends Ref.AnyConfectPublicPaginatedQuery> =
|
|
189
215
|
| FunctionError<Query>
|
|
190
216
|
| PaginationError.InvalidCursor
|
|
@@ -205,7 +231,9 @@ type MachineActive<Query extends Ref.AnyConfectPublicPaginatedQuery> = Active<
|
|
|
205
231
|
Error<Query>
|
|
206
232
|
>;
|
|
207
233
|
|
|
208
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* The schema-and-constructor bundle returned by `make`.
|
|
236
|
+
*/
|
|
209
237
|
export interface PaginatedQuery<
|
|
210
238
|
Query extends Ref.AnyConfectPublicPaginatedQuery,
|
|
211
239
|
> {
|
|
@@ -295,7 +323,9 @@ const SerializableSchemaError = Schema.instanceOf(Schema.SchemaError, {
|
|
|
295
323
|
}),
|
|
296
324
|
});
|
|
297
325
|
|
|
298
|
-
/**
|
|
326
|
+
/**
|
|
327
|
+
* Builds a page machine from a paginated ref.
|
|
328
|
+
*/
|
|
299
329
|
export const make = <Query extends Ref.AnyConfectPublicPaginatedQuery>(
|
|
300
330
|
ref: Query,
|
|
301
331
|
): PaginatedQuery<Query> => {
|
|
@@ -483,7 +513,9 @@ const descriptorEquals = (a: PageDescriptor, b: PageDescriptor): boolean =>
|
|
|
483
513
|
}),
|
|
484
514
|
});
|
|
485
515
|
|
|
486
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* Returns the logical request currently driving the subscription.
|
|
518
|
+
*/
|
|
487
519
|
export const getSubscriptionRequest = <Item_, UserArgs_, Error_>(
|
|
488
520
|
state: Active<Item_, UserArgs_, Error_>,
|
|
489
521
|
): SubscriptionRequest<UserArgs_> => ({
|
|
@@ -495,7 +527,9 @@ export const getSubscriptionRequest = <Item_, UserArgs_, Error_>(
|
|
|
495
527
|
descriptor: state.current,
|
|
496
528
|
});
|
|
497
529
|
|
|
498
|
-
/**
|
|
530
|
+
/**
|
|
531
|
+
* Attaches the client-allocated pagination id to a logical request.
|
|
532
|
+
*/
|
|
499
533
|
export const allocateRequest = <UserArgs_>(
|
|
500
534
|
request: SubscriptionRequest<UserArgs_>,
|
|
501
535
|
paginationId: number,
|
|
@@ -525,12 +559,16 @@ const pendingPhase = <Item_>(
|
|
|
525
559
|
onSome: (page) => Phase.Refreshing<Item_, never>({ data: page, direction }),
|
|
526
560
|
});
|
|
527
561
|
|
|
528
|
-
/**
|
|
562
|
+
/**
|
|
563
|
+
* Close the subscription while retaining its generation tombstone.
|
|
564
|
+
*/
|
|
529
565
|
export const close = <Item_, UserArgs_, Error_>(
|
|
530
566
|
state: Active<Item_, UserArgs_, Error_>,
|
|
531
567
|
): Idle => State.Idle({ generation: state.generation });
|
|
532
568
|
|
|
533
|
-
/**
|
|
569
|
+
/**
|
|
570
|
+
* Start a fresh pagination session at page one while retaining visible data.
|
|
571
|
+
*/
|
|
534
572
|
export const reset = <Item_, UserArgs_, Error_>(
|
|
535
573
|
state: Active<Item_, UserArgs_, Error_>,
|
|
536
574
|
): Active<Item_, UserArgs_, Error_> => ({
|
|
@@ -546,13 +584,13 @@ export const reset = <Item_, UserArgs_, Error_>(
|
|
|
546
584
|
/**
|
|
547
585
|
* Navigate to the next page, retaining the current page while it loads.
|
|
548
586
|
*
|
|
549
|
-
* The page being left is pushed onto the stack
|
|
550
|
-
* displayed—its cursor to its continuation cursor—so `prev` reloads
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
587
|
+
* The page being left is pushed onto the stack _pinned_ to the range it
|
|
588
|
+
* displayed—its cursor to its continuation cursor—so `prev` reloads exactly
|
|
589
|
+
* that range rather than the first `initialNumItems` documents after its
|
|
590
|
+
* cursor, however the data has moved meanwhile. Convex's own pagination keeps
|
|
591
|
+
* that range in its query journal; stream-paginated queries have no journal, so
|
|
592
|
+
* the pin is what keeps consecutive pages gap-free and duplicate-free for
|
|
593
|
+
* them.
|
|
556
594
|
*/
|
|
557
595
|
export const next = <Item_, UserArgs_, Error_>(
|
|
558
596
|
state: Active<Item_, UserArgs_, Error_>,
|
|
@@ -589,7 +627,9 @@ export const next = <Item_, UserArgs_, Error_>(
|
|
|
589
627
|
Match.exhaustive,
|
|
590
628
|
);
|
|
591
629
|
|
|
592
|
-
/**
|
|
630
|
+
/**
|
|
631
|
+
* Navigate to the previous page, retaining the current page while it loads.
|
|
632
|
+
*/
|
|
593
633
|
export const prev = <Item_, UserArgs_, Error_>(
|
|
594
634
|
state: Active<Item_, UserArgs_, Error_>,
|
|
595
635
|
): Option.Option<Active<Item_, UserArgs_, Error_>> =>
|
|
@@ -619,7 +659,9 @@ export const prev = <Item_, UserArgs_, Error_>(
|
|
|
619
659
|
Match.exhaustive,
|
|
620
660
|
);
|
|
621
661
|
|
|
622
|
-
/**
|
|
662
|
+
/**
|
|
663
|
+
* Navigate to page one without invalidating the current pagination session.
|
|
664
|
+
*/
|
|
623
665
|
export const first = <Item_, UserArgs_, Error_>(
|
|
624
666
|
state: Active<Item_, UserArgs_, Error_>,
|
|
625
667
|
): Option.Option<Active<Item_, UserArgs_, Error_>> =>
|
|
@@ -641,7 +683,9 @@ export const first = <Item_, UserArgs_, Error_>(
|
|
|
641
683
|
Match.exhaustive,
|
|
642
684
|
);
|
|
643
685
|
|
|
644
|
-
/**
|
|
686
|
+
/**
|
|
687
|
+
* Whether a request still names the machine's live pagination session/page.
|
|
688
|
+
*/
|
|
645
689
|
export const isCurrentRequest = <Item_, UserArgs_, Error_>(
|
|
646
690
|
state: State<Item_, UserArgs_, Error_>,
|
|
647
691
|
candidate: Request<UserArgs_>,
|
|
@@ -810,7 +854,9 @@ export const settle: {
|
|
|
810
854
|
),
|
|
811
855
|
);
|
|
812
856
|
|
|
813
|
-
/**
|
|
857
|
+
/**
|
|
858
|
+
* The complete page currently available to render.
|
|
859
|
+
*/
|
|
814
860
|
export const getPage = <Item_, UserArgs_, Error_>(
|
|
815
861
|
state: State<Item_, UserArgs_, Error_>,
|
|
816
862
|
): Option.Option<Page<Item_>> =>
|
|
@@ -820,13 +866,17 @@ export const getPage = <Item_, UserArgs_, Error_>(
|
|
|
820
866
|
Match.exhaustive,
|
|
821
867
|
);
|
|
822
868
|
|
|
823
|
-
/**
|
|
869
|
+
/**
|
|
870
|
+
* The items currently available to render.
|
|
871
|
+
*/
|
|
824
872
|
export const getItems = <Item_, UserArgs_, Error_>(
|
|
825
873
|
state: State<Item_, UserArgs_, Error_>,
|
|
826
874
|
): Option.Option<ReadonlyArray<Item_>> =>
|
|
827
875
|
Option.map(getPage(state), (page) => page.items);
|
|
828
876
|
|
|
829
|
-
/**
|
|
877
|
+
/**
|
|
878
|
+
* The error from the most recent failed request, if any.
|
|
879
|
+
*/
|
|
830
880
|
export const getError = <Item_, UserArgs_, Error_>(
|
|
831
881
|
state: State<Item_, UserArgs_, Error_>,
|
|
832
882
|
): Option.Option<Error_> =>
|
|
@@ -844,7 +894,9 @@ export const getError = <Item_, UserArgs_, Error_>(
|
|
|
844
894
|
Match.exhaustive,
|
|
845
895
|
);
|
|
846
896
|
|
|
847
|
-
/**
|
|
897
|
+
/**
|
|
898
|
+
* The 1-indexed page number currently targeted by the subscription.
|
|
899
|
+
*/
|
|
848
900
|
export const targetPageNumber = <Item_, UserArgs_, Error_>(
|
|
849
901
|
state: Active<Item_, UserArgs_, Error_>,
|
|
850
902
|
): number => state.prevStack.length + 1;
|
|
@@ -959,7 +1011,9 @@ export const canPrev = <Item_, UserArgs_, Error_>(
|
|
|
959
1011
|
Match.exhaustive,
|
|
960
1012
|
);
|
|
961
1013
|
|
|
962
|
-
/**
|
|
1014
|
+
/**
|
|
1015
|
+
* Pattern-match exhaustively on the machine's AsyncData-style state.
|
|
1016
|
+
*/
|
|
963
1017
|
export const match: {
|
|
964
1018
|
<Item_, Error_, A, B, C, D, E, F>(handlers: {
|
|
965
1019
|
readonly onIdle: (idle: Idle) => A;
|