@foir/sdk 0.7.0 → 0.9.0
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 +857 -4
- package/README.md +46 -15
- package/dist/auth/auth.d.ts +104 -1
- package/dist/auth/hosted.d.ts +22 -0
- package/dist/auth/index.d.ts +13 -7
- package/dist/auth/operations.d.ts +85 -64
- package/dist/auth/ops.d.ts +99 -0
- package/dist/auth.js +786 -106
- package/dist/chunk-IGNLWT3H.js +35 -0
- package/dist/chunk-OHYVKTGB.js +45 -0
- package/dist/chunk-TAL5H6VA.js +39 -0
- package/dist/client.js +25 -31
- package/dist/codegen/block-registry.d.ts +13 -19
- package/dist/codegen/block-registry.js +3 -31
- package/dist/codegen/block-scaffold.d.ts +12 -21
- package/dist/codegen/block-scaffold.js +3 -41
- package/dist/codegen/index.d.ts +9 -4
- package/dist/codegen/index.js +7 -11
- package/dist/codegen/item-registry.d.ts +28 -0
- package/dist/codegen/item-registry.js +7 -0
- package/dist/codegen/item-scaffold.d.ts +29 -0
- package/dist/codegen/item-scaffold.js +7 -0
- package/dist/codegen/preset.d.ts +14 -7
- package/dist/codegen/shared.d.ts +20 -9
- package/dist/content/content-renderer.d.ts +90 -24
- package/dist/content/index.d.ts +13 -7
- package/dist/content/richtext.d.ts +66 -18
- package/dist/content.js +227 -80
- package/dist/data/builtins.d.ts +369 -92
- package/dist/data/client.d.ts +97 -25
- package/dist/data/document.d.ts +251 -0
- package/dist/data/index.d.ts +38 -19
- package/dist/data/manifest.d.ts +290 -8
- package/dist/data/schema.d.ts +78 -8
- package/dist/data/selection.d.ts +231 -37
- package/dist/data/subjects.d.ts +221 -0
- package/dist/data/subscription-document.d.ts +75 -0
- package/dist/data/subscriptions.d.ts +181 -0
- package/dist/data/transport.d.ts +255 -37
- package/dist/data/ws-transport.d.ts +197 -0
- package/dist/data.js +1419 -45
- package/dist/graphql/cdn-route.d.ts +12 -28
- package/dist/media/index.d.ts +6 -2
- package/dist/media/media.d.ts +2 -2
- package/dist/media/screen-capture.d.ts +81 -0
- package/dist/media.js +97 -0
- package/dist/next.js +663 -182
- package/dist/push.js +700 -90
- package/dist/react/FoirFeedback.d.ts +62 -0
- package/dist/react/feedback-notes.d.ts +101 -0
- package/dist/react/index.d.ts +7 -1
- package/dist/react.js +1272 -194
- package/dist/realtime.js +1 -1
- package/dist/server/events-client.d.ts +279 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server.js +298 -0
- package/dist/transport/foir-http.d.ts +23 -6
- package/dist/transport/persisted-query.d.ts +68 -0
- package/dist/transport/retry.d.ts +1 -1
- package/package.json +13 -1
- package/dist/chunk-ADO5FJNM.js +0 -30
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Printing a subscription document from a resolved selection tree.
|
|
3
|
+
*
|
|
4
|
+
* ---------------------------------------------------------------------------
|
|
5
|
+
* Why this file is thin
|
|
6
|
+
* ---------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* The body of a subscription document is a selection body like any other, and
|
|
9
|
+
* `document.ts` already walks one: relations skipped, `truncated` degraded to
|
|
10
|
+
* `__typename`, unions expanded to `__typename` plus an inline fragment per
|
|
11
|
+
* member, aliases printed as `alias: key`. Those rules belong to the tree, not
|
|
12
|
+
* to the operation type, so this file does NOT re-implement them — it calls
|
|
13
|
+
* `printFoirSelectionBody` and adds only what is genuinely different about a
|
|
14
|
+
* subscription: the `subscription` keyword, the variable declarations, and the
|
|
15
|
+
* single root field.
|
|
16
|
+
*
|
|
17
|
+
* That split is deliberate. This file previously carried its own copy of the
|
|
18
|
+
* walk, written before `document.ts` existed; two implementations of one rule
|
|
19
|
+
* set in one directory is the parallel construction this epic exists to remove,
|
|
20
|
+
* and the copy would have been correct only until the next rule changed.
|
|
21
|
+
*
|
|
22
|
+
* Nothing here inspects a GraphQL type, and nothing here may start to. The
|
|
23
|
+
* server resolves the tree once per schema version — unions expanded, aliases
|
|
24
|
+
* assigned, cycles broken, budget counted — so printing is string building. If
|
|
25
|
+
* this ever needs to know what a field IS, the tree is under-specified and the
|
|
26
|
+
* fix belongs in the manifest.
|
|
27
|
+
*
|
|
28
|
+
* ---------------------------------------------------------------------------
|
|
29
|
+
* Why it is not APQ
|
|
30
|
+
* ---------------------------------------------------------------------------
|
|
31
|
+
*
|
|
32
|
+
* The HTTP lane hashes its document and sends the hash, because it re-sends the
|
|
33
|
+
* same document on every request. graphql-ws sends the document ONCE, in the
|
|
34
|
+
* `subscribe` frame, and the server then pushes events over it for the life of
|
|
35
|
+
* the subscription. The wire-cost argument does not apply to a stream, so the
|
|
36
|
+
* document goes whole and the server needs no registry to serve it.
|
|
37
|
+
*/
|
|
38
|
+
import { type FoirSelectionUnions } from './document';
|
|
39
|
+
import type { FoirManifestSelection } from './manifest';
|
|
40
|
+
/**
|
|
41
|
+
* One argument a subscription root field is called with, bound to a variable.
|
|
42
|
+
*
|
|
43
|
+
* Variables rather than inline literals because the values are caller data:
|
|
44
|
+
* inlining would mean escaping a string into a document, which is the one place
|
|
45
|
+
* a client can construct an injection. The `subscribe` frame carries a
|
|
46
|
+
* `variables` object beside the query, so there is no reason to inline.
|
|
47
|
+
*/
|
|
48
|
+
export interface FoirSubscriptionArgument {
|
|
49
|
+
/** Argument name, e.g. `modelKey`. */
|
|
50
|
+
name: string;
|
|
51
|
+
/** GraphQL type for the variable declaration, e.g. `String!`. */
|
|
52
|
+
type: string;
|
|
53
|
+
}
|
|
54
|
+
export interface PrintSubscriptionDocumentInput {
|
|
55
|
+
/** The Subscription root field to select. */
|
|
56
|
+
field: string;
|
|
57
|
+
/** Operation name, for readability in server logs and dev tools. */
|
|
58
|
+
operationName: string;
|
|
59
|
+
/** Arguments the field takes, in the order they should be declared. */
|
|
60
|
+
args?: readonly FoirSubscriptionArgument[];
|
|
61
|
+
/** The payload's resolved selection. */
|
|
62
|
+
selection: FoirManifestSelection;
|
|
63
|
+
/** Union expansions the selection references by name. */
|
|
64
|
+
unions?: FoirSelectionUnions;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Print a complete subscription document.
|
|
68
|
+
*
|
|
69
|
+
* Deterministic: the same inputs always produce the same bytes, because the
|
|
70
|
+
* server sorts union members and emits fields in a stable order. That matters
|
|
71
|
+
* for readable diffs in tests and for anything that ever wants to recognise a
|
|
72
|
+
* document, and it costs nothing to preserve — so this must never reorder
|
|
73
|
+
* anything it is handed.
|
|
74
|
+
*/
|
|
75
|
+
export declare function printSubscriptionDocument(input: PrintSubscriptionDocumentInput): string;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription addressing — the five Subscription root fields, reached through
|
|
3
|
+
* the client shape that already exists.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const live = foir.page.onChange({ onData: (e) => revalidate(e.recordId) });
|
|
7
|
+
* const inbox = foir.ns.notifications.onNotification({ onData: show });
|
|
8
|
+
* const run = foir.ns.operations.onExecution({ id, onData: render });
|
|
9
|
+
* live.unsubscribe();
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* ---------------------------------------------------------------------------
|
|
13
|
+
* Why the addressing is what it is
|
|
14
|
+
* ---------------------------------------------------------------------------
|
|
15
|
+
*
|
|
16
|
+
* `recordChanged` takes the model as an ARGUMENT, and the argument is exactly
|
|
17
|
+
* what the client's model accessor already carries — `foir.page` IS
|
|
18
|
+
* `modelKey: "page"`. So it hangs off the model, and the one argument the
|
|
19
|
+
* server gates on is supplied by the address rather than by the caller. That is
|
|
20
|
+
* also why a model the credential may not watch is a compile-time absence
|
|
21
|
+
* rather than a runtime refusal: it is not in `FoirSchema`.
|
|
22
|
+
*
|
|
23
|
+
* The other four are project-independent platform surfaces, so they hang off
|
|
24
|
+
* the namespace that owns them: `notification` under `notifications`,
|
|
25
|
+
* `jobProgress` and both execution streams under `operations`.
|
|
26
|
+
*
|
|
27
|
+
* Every subscription method is named `on…`, so a call site that opens a stream
|
|
28
|
+
* is distinguishable at a glance from one that awaits a value — and no
|
|
29
|
+
* generated op name can collide, because the server never emits a root field
|
|
30
|
+
* called `on…`.
|
|
31
|
+
*
|
|
32
|
+
* ---------------------------------------------------------------------------
|
|
33
|
+
* Where the document comes from
|
|
34
|
+
* ---------------------------------------------------------------------------
|
|
35
|
+
*
|
|
36
|
+
* From `GET /v1/manifest`, through the CLIENT's memoised loader — the same one
|
|
37
|
+
* the query path falls back to and the drift check reads. Each subscription
|
|
38
|
+
* carries a resolved selection tree, which `subscription-document.ts` prints
|
|
39
|
+
* with the same walk the query path prints models with.
|
|
40
|
+
*
|
|
41
|
+
* A subscribing client therefore fetches the descriptor once even when
|
|
42
|
+
* `config.selections` was supplied, because `selections` carries MODEL trees and
|
|
43
|
+
* nothing generates subscription ones yet. That is a real cost and it is stated
|
|
44
|
+
* rather than hidden — but it is ONE fetch, memoised across every subscription
|
|
45
|
+
* and shared with the query path, sitting behind a long-lived socket. `client.ts`
|
|
46
|
+
* rejects a round trip in front of every query for good reason; in front of a
|
|
47
|
+
* socket that will live for minutes it is noise.
|
|
48
|
+
*/
|
|
49
|
+
import type { FoirManifest } from './manifest';
|
|
50
|
+
import type { FoirClientConfig } from './transport';
|
|
51
|
+
import { type FoirSubscription, type FoirSubscriptionHandlers } from './ws-transport';
|
|
52
|
+
/** `recordChanged` — a record in one model was created, updated or removed. */
|
|
53
|
+
export interface FoirRecordChangedEvent {
|
|
54
|
+
recordId: string;
|
|
55
|
+
modelKey: string;
|
|
56
|
+
/** `created`, `updated`, `published`, `unpublished` or `deleted`. */
|
|
57
|
+
action: string;
|
|
58
|
+
/** Actor, when a user made the change. Absent for system and service callers. */
|
|
59
|
+
userId?: string | null;
|
|
60
|
+
/** The durable event this projects — the same id a webhook delivery carries. */
|
|
61
|
+
eventId?: string | null;
|
|
62
|
+
/** Event family name, e.g. `RECORD_UPDATED`. The name a hook subscribes to. */
|
|
63
|
+
event?: string | null;
|
|
64
|
+
/**
|
|
65
|
+
* Platform sequence, assigned in commit order and monotonic per project.
|
|
66
|
+
* Apply last-write-wins: keep the highest seq applied per record and discard
|
|
67
|
+
* anything at or below it.
|
|
68
|
+
*/
|
|
69
|
+
seq?: number | null;
|
|
70
|
+
}
|
|
71
|
+
/** `notification` — one notification for the subscribing user. */
|
|
72
|
+
export interface FoirNotificationEvent {
|
|
73
|
+
userId: string;
|
|
74
|
+
/** `notification`, `announcement` or `mention`. */
|
|
75
|
+
type: string;
|
|
76
|
+
title?: string | null;
|
|
77
|
+
}
|
|
78
|
+
/** `jobProgress` — a background job's lifecycle. */
|
|
79
|
+
export interface FoirJobProgressEvent {
|
|
80
|
+
jobId: string;
|
|
81
|
+
queue?: string | null;
|
|
82
|
+
/** `started`, `progress`, `completed` or `failed`. */
|
|
83
|
+
status: string;
|
|
84
|
+
/** 0-100 when reported. */
|
|
85
|
+
progress?: number | null;
|
|
86
|
+
error?: string | null;
|
|
87
|
+
}
|
|
88
|
+
/** `operationExecution` / `operation{Op}Execution` — one execution's progress. */
|
|
89
|
+
export interface FoirOperationExecutionEvent {
|
|
90
|
+
id: string;
|
|
91
|
+
/** `pending`, `dispatched`, `running`, `completed`, `failed`, `cancelled` or `timed_out`. */
|
|
92
|
+
status: string;
|
|
93
|
+
/** 0-100; null on terminal events. */
|
|
94
|
+
progressPct?: number | null;
|
|
95
|
+
progressMessage?: string | null;
|
|
96
|
+
progressMetadata?: unknown;
|
|
97
|
+
progressUpdatedAt?: string | null;
|
|
98
|
+
/** Present on `status: completed`. Typed per operation only on the typed field. */
|
|
99
|
+
resultJson?: unknown;
|
|
100
|
+
/** Present on `status: failed` or `timed_out`. */
|
|
101
|
+
errorJson?: unknown;
|
|
102
|
+
}
|
|
103
|
+
/** Options for `foir.ns.operations.onExecution`. */
|
|
104
|
+
export type FoirExecutionSubscribeOptions = FoirSubscriptionHandlers<FoirOperationExecutionEvent> & {
|
|
105
|
+
/** Execution id, as returned by `execute<Op>`. */
|
|
106
|
+
id: string;
|
|
107
|
+
/**
|
|
108
|
+
* Operation key, to subscribe to that operation's TYPED stream
|
|
109
|
+
* (`operation{Op}Execution`) instead of the generic one.
|
|
110
|
+
*
|
|
111
|
+
* The key is matched against the field names the manifest publishes rather
|
|
112
|
+
* than converted into one: the server owns the PascalCase rule, and
|
|
113
|
+
* re-deriving it here is the naming drift the SDK contract forbids. An
|
|
114
|
+
* operation with no typed subscription fails with the list of fields that do
|
|
115
|
+
* exist, rather than silently opening the generic stream under a name the
|
|
116
|
+
* caller did not ask for.
|
|
117
|
+
*/
|
|
118
|
+
operation?: string;
|
|
119
|
+
};
|
|
120
|
+
/** The subscriptions a model accessor carries. */
|
|
121
|
+
export interface FoirModelSubscriptions {
|
|
122
|
+
/**
|
|
123
|
+
* Stream changes to this model's records.
|
|
124
|
+
*
|
|
125
|
+
* The socket refuses a model this credential may not read, but the model is
|
|
126
|
+
* not in `FoirSchema` in that case, so the refusal is a type error first.
|
|
127
|
+
*/
|
|
128
|
+
onChange(handlers: FoirSubscriptionHandlers<FoirRecordChangedEvent>): FoirSubscription;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The subscriptions one namespace carries, by name.
|
|
132
|
+
*
|
|
133
|
+
* Keyed on the namespace NAME rather than added to every namespace, because
|
|
134
|
+
* `foir.ns.search.onNotification` should not type-check. The empty branch is
|
|
135
|
+
* what every other namespace gets.
|
|
136
|
+
*/
|
|
137
|
+
export type FoirNamespaceSubscriptions<NK> = NK extends 'notifications' ? {
|
|
138
|
+
/** Stream this caller's own notifications. Scoped server-side; no arguments. */
|
|
139
|
+
onNotification(handlers: FoirSubscriptionHandlers<FoirNotificationEvent>): FoirSubscription;
|
|
140
|
+
} : NK extends 'operations' ? {
|
|
141
|
+
/** Stream background-job lifecycle events for this project. */
|
|
142
|
+
onJobProgress(handlers: FoirSubscriptionHandlers<FoirJobProgressEvent>): FoirSubscription;
|
|
143
|
+
/** Stream one operation execution's progress until it reaches a terminal state. */
|
|
144
|
+
onExecution(options: FoirExecutionSubscribeOptions): FoirSubscription;
|
|
145
|
+
} : {};
|
|
146
|
+
/** Does this (address, property) pair name a subscription rather than an op? */
|
|
147
|
+
export declare function isModelSubscriptionMethod(name: string): boolean;
|
|
148
|
+
export declare function isNamespaceSubscriptionMethod(namespace: string, name: string): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* The runtime half: resolves a call site to a route, prints its document from
|
|
151
|
+
* the manifest, and hands it to the socket.
|
|
152
|
+
*
|
|
153
|
+
* One instance per client, created lazily — a client that never subscribes
|
|
154
|
+
* neither fetches a manifest nor opens a socket.
|
|
155
|
+
*/
|
|
156
|
+
export declare class FoirSubscriptionRouter {
|
|
157
|
+
private readonly transport;
|
|
158
|
+
private readonly loadManifest;
|
|
159
|
+
/**
|
|
160
|
+
* `loadManifest` is the client's own memoised descriptor loader, passed in
|
|
161
|
+
* rather than rebuilt. One client is one credential and one descriptor; a
|
|
162
|
+
* fetcher of our own would race the query path's for the same bytes and could
|
|
163
|
+
* end up holding a different schema version than the documents the query path
|
|
164
|
+
* is printing from.
|
|
165
|
+
*/
|
|
166
|
+
constructor(config: FoirClientConfig, loadManifest: () => Promise<FoirManifest>);
|
|
167
|
+
dispose(): void;
|
|
168
|
+
/** `foir.<model>.onChange(...)`. */
|
|
169
|
+
model(modelKey: string, method: string, input: Record<string, unknown>): FoirSubscription;
|
|
170
|
+
/** `foir.ns.<namespace>.on…(...)`. */
|
|
171
|
+
namespace(namespace: string, method: string, input: Record<string, unknown>): FoirSubscription;
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the route's document and subscribe.
|
|
174
|
+
*
|
|
175
|
+
* The handle is returned synchronously and the manifest fetch runs behind it,
|
|
176
|
+
* so a caller can hold and cancel a subscription before the descriptor has
|
|
177
|
+
* arrived. Cancelling in that window is honoured: `cancelled` is checked
|
|
178
|
+
* after the await, and the inner handle is never created.
|
|
179
|
+
*/
|
|
180
|
+
private open;
|
|
181
|
+
}
|
package/dist/data/transport.d.ts
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The data client's transport: two GraphQL lanes, and no other.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* ---------------------------------------------------------------------------
|
|
5
|
+
* Where a call goes
|
|
6
|
+
* ---------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* read + `pk_` key + no customer bearer + published channel
|
|
9
|
+
* → `GET /cdn/graphql?key=pk_…&extensions={"persistedQuery":{"sha256Hash":…}}`
|
|
10
|
+
* A shared cache in front of the caller stores the response, because the
|
|
11
|
+
* request is a GET and the credential is in the URL.
|
|
12
|
+
*
|
|
13
|
+
* anything else — `sk_`, a customer bearer, a mutation, the draft channel
|
|
14
|
+
* → `POST /graphql` with the `x-api-key` header and the APQ body.
|
|
15
|
+
*
|
|
16
|
+
* All five conditions the server's shared-lane gate refuses on are evaluated
|
|
17
|
+
* HERE, locally: this code is constructing the URL, it can read the key prefix,
|
|
18
|
+
* it knows whether a bearer resolved, the manifest told it whether the op is a
|
|
19
|
+
* mutation, and the channel is its own configuration. No schema, no extra round
|
|
20
|
+
* trip. `foirRoute` is that decision, isolated so each condition can be tested on
|
|
21
|
+
* its own rather than inferred from an end-to-end call that happened to take one
|
|
22
|
+
* path.
|
|
23
|
+
*
|
|
24
|
+
* The server remains the authority. It REFUSES an ineligible request rather than
|
|
25
|
+
* serving it uncached (`NOT_SHAREABLE`, with the condition named), and a refusal
|
|
26
|
+
* is retried on `/graphql` — but eligibility is never discovered by trial.
|
|
27
|
+
*
|
|
28
|
+
* ---------------------------------------------------------------------------
|
|
29
|
+
* Persisted queries
|
|
30
|
+
* ---------------------------------------------------------------------------
|
|
31
|
+
*
|
|
32
|
+
* Complete-by-default produces maximal documents: `page.findMany` prints ~34kB.
|
|
33
|
+
* So the hash goes first and the document follows only on a miss, on both lanes.
|
|
34
|
+
* A miss answers `PERSISTED_QUERY_NOT_FOUND` with **HTTP 200**, and the retry
|
|
35
|
+
* that carries the document is always a POST — never a longer GET, which is the
|
|
36
|
+
* limit the hash exists to stay under.
|
|
37
|
+
*
|
|
38
|
+
* ---------------------------------------------------------------------------
|
|
39
|
+
* One wire format, and no second one
|
|
40
|
+
* ---------------------------------------------------------------------------
|
|
41
|
+
*
|
|
42
|
+
* Every op prints. The manifest publishes a `payload` for each op a credential
|
|
43
|
+
* can reach — every namespace op included, and the model ops whose result is not
|
|
44
|
+
* the model — so no call has to be described some other way. An op this client
|
|
45
|
+
* cannot print is an error that names the op.
|
|
46
|
+
*
|
|
47
|
+
* That is a constraint the module holds to, not an accident of what exists: a
|
|
48
|
+
* second format kept alive beside this one could only ever be reached as a
|
|
49
|
+
* silent fallback, and an op that quietly stopped being printed would land there
|
|
50
|
+
* unnoticed.
|
|
9
51
|
*
|
|
10
52
|
* Edge-safe: `fetch` only, no Node built-ins, and no module-level state of any
|
|
11
53
|
* kind. A shared cache on a stateless multi-tenant edge worker is a
|
|
@@ -14,29 +56,51 @@
|
|
|
14
56
|
* across instances is cached across tenants.
|
|
15
57
|
*/
|
|
16
58
|
import { type FoirTokenProvider } from '../transport/foir-http';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
import type { FoirDocumentPlan, FoirNamespaceSelections, FoirSelections, FoirSelectionUnions } from './document';
|
|
60
|
+
import type { FoirSchemaDefinition } from './schema';
|
|
61
|
+
import type { FoirSubjectRegistry } from './subjects';
|
|
62
|
+
import type { FoirReconnectPolicy, FoirWebSocketFactory } from './ws-transport';
|
|
63
|
+
/**
|
|
64
|
+
* `foir.generated.ts`, whole — the shape a namespace import of it has.
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* import * as generated from './foir.generated';
|
|
68
|
+
* const foir = createFoirClient({ apiUrl, apiKey, generated });
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* Every member is REQUIRED, and that is the point rather than an oversight.
|
|
72
|
+
* `foir types` emits all of them together and there is no case where a consumer
|
|
73
|
+
* wants a subset, so a module missing one is stale or hand-assembled — and the
|
|
74
|
+
* cost of accepting it quietly is the failure this type exists to stop:
|
|
75
|
+
* `foirSelections` once shipped in a shape the printer could not read, every
|
|
76
|
+
* call fell through to `GET /v1/manifest`, and nothing said so. A partial bundle
|
|
77
|
+
* is a compile error here and a refusal at construction, never a fallback.
|
|
78
|
+
*
|
|
79
|
+
* `foirSchemaType` is how `S` is inferred. A namespace import carries VALUES,
|
|
80
|
+
* and `FoirSchema` is an interface — it does not survive to runtime — so without
|
|
81
|
+
* a value whose TYPE is `FoirSchema` there would be nothing to infer from and
|
|
82
|
+
* every call site would go back to restating `createFoirClient<FoirSchema>`.
|
|
83
|
+
* Its runtime value is `undefined` by design and nothing reads it.
|
|
84
|
+
*/
|
|
85
|
+
export interface FoirGeneratedModule<S extends FoirSchemaDefinition = FoirSchemaDefinition> {
|
|
86
|
+
/** `foirSchemaVersion` — the manifest version these types came from. Drives the drift check. */
|
|
87
|
+
readonly foirSchemaVersion: number;
|
|
88
|
+
/** `foirSelections` — one entry per model, in the printer's own input shape. */
|
|
89
|
+
readonly foirSelections: FoirSelections;
|
|
90
|
+
/** `foirNamespaceSelections` — the same, per namespace op. */
|
|
91
|
+
readonly foirNamespaceSelections: FoirNamespaceSelections;
|
|
92
|
+
/** `foirSelectionUnions` — each union expanded once; a tree references one by name. */
|
|
93
|
+
readonly foirSelectionUnions: FoirSelectionUnions;
|
|
94
|
+
/** Type carrier for `FoirSchema`. Never read: its runtime value is `undefined`. */
|
|
95
|
+
readonly foirSchemaType: S;
|
|
96
|
+
}
|
|
97
|
+
export interface FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDefinition> {
|
|
34
98
|
/**
|
|
35
99
|
* Foir public API origin, e.g. `https://api.foir.dev`.
|
|
36
100
|
*
|
|
37
101
|
* A full GraphQL endpoint is accepted too — `PLATFORM_API_URL` is
|
|
38
102
|
* conventionally `https://api.foir.dev/graphql` and people will paste it — so
|
|
39
|
-
* a trailing `/graphql` is stripped before
|
|
103
|
+
* a trailing `/graphql` is stripped before a path is appended.
|
|
40
104
|
*/
|
|
41
105
|
apiUrl: string;
|
|
42
106
|
/** `pk_…` or `sk_…`. The manifest, and therefore the generated types, are shaped by it. */
|
|
@@ -53,6 +117,76 @@ export interface FoirClientConfig {
|
|
|
53
117
|
* per-request; a bare string is used as-is.
|
|
54
118
|
*/
|
|
55
119
|
bearerToken?: string | FoirTokenProvider;
|
|
120
|
+
/**
|
|
121
|
+
* The generated module, whole — **the documented way to wire a client**.
|
|
122
|
+
*
|
|
123
|
+
* ```ts
|
|
124
|
+
* import * as generated from './foir.generated';
|
|
125
|
+
* const foir = createFoirClient({ apiUrl, apiKey, generated });
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* It supplies `schemaVersion`, `selections`, `namespaceSelections` and
|
|
129
|
+
* `selectionUnions` in one member, and carries `FoirSchema` with it so the
|
|
130
|
+
* type parameter is inferred rather than restated. An artifact added to
|
|
131
|
+
* `foir types` later then costs a call site nothing.
|
|
132
|
+
*
|
|
133
|
+
* The four fields below still work and still win where both are given, so a
|
|
134
|
+
* client written against the older shape is untouched. What they cannot do is
|
|
135
|
+
* be individually forgotten: `foir types` emits all of them together, four out
|
|
136
|
+
* of five was always a mistake, and it used to degrade into a silent manifest
|
|
137
|
+
* fetch. Passing the module makes that a refusal instead.
|
|
138
|
+
*
|
|
139
|
+
* Omitting `generated` AND `selections` asks for the manifest fallback, which
|
|
140
|
+
* is supported, announced (see `announceManifestFallback`) and never reached
|
|
141
|
+
* by accident.
|
|
142
|
+
*/
|
|
143
|
+
generated?: FoirGeneratedModule<S>;
|
|
144
|
+
/**
|
|
145
|
+
* The runtime half of `foir.generated.ts` — `foirSelections`, the resolved
|
|
146
|
+
* complete-by-default selection per model.
|
|
147
|
+
*
|
|
148
|
+
* Pass it, or pass `generated`, which carries it. Without either the client
|
|
149
|
+
* fetches `GET /v1/manifest` once at first use to get the same trees, which
|
|
150
|
+
* works and is the reason a zero-codegen start still functions, but it puts a
|
|
151
|
+
* round trip in front of the first query on every cold edge invocation to
|
|
152
|
+
* learn something the generated file already knows.
|
|
153
|
+
*/
|
|
154
|
+
selections?: FoirSelections;
|
|
155
|
+
/**
|
|
156
|
+
* `foirNamespaceSelections` from the same generated module — the namespace
|
|
157
|
+
* half of the surface, one `payload` per namespace op.
|
|
158
|
+
*
|
|
159
|
+
* Read only when `selections` is passed; the manifest fallback carries both.
|
|
160
|
+
* Omitting it under `selections` makes every `foir.ns.*` call an error naming
|
|
161
|
+
* the namespace, which is the honest failure for a generated module that
|
|
162
|
+
* predates namespace payloads.
|
|
163
|
+
*/
|
|
164
|
+
namespaceSelections?: FoirNamespaceSelections;
|
|
165
|
+
/** `foirSelectionUnions` from the same generated module. Required whenever a tree names a union. */
|
|
166
|
+
selectionUnions?: FoirSelectionUnions;
|
|
167
|
+
/**
|
|
168
|
+
* A registry that records which records this client fetched, so an annotation
|
|
169
|
+
* surface can anchor to the RECORD rather than to a URL or a CSS selector.
|
|
170
|
+
*
|
|
171
|
+
* ```ts
|
|
172
|
+
* const subjects = createFoirSubjectRegistry();
|
|
173
|
+
* const foir = createFoirClient({ apiUrl, apiKey, generated, subjects });
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* There is no boolean to set. A registry is a list of what a page fetched,
|
|
177
|
+
* which is a disclosure surface — so it exists only when the host builds one,
|
|
178
|
+
* the host holds the only reference, and its lifetime is the host's. Omitting
|
|
179
|
+
* this field is the off state, and no other input reaches it.
|
|
180
|
+
*
|
|
181
|
+
* Build one per client, never at module scope: a shared registry on a
|
|
182
|
+
* multi-tenant edge worker is shared across tenants, for the same reason
|
|
183
|
+
* nothing else in this client keeps module-level state.
|
|
184
|
+
*
|
|
185
|
+
* `foir.raw()` does not record — a hand-written document has no published
|
|
186
|
+
* payload, so there are no record positions to read. Seed those with
|
|
187
|
+
* `subjects.add(...)`.
|
|
188
|
+
*/
|
|
189
|
+
subjects?: FoirSubjectRegistry;
|
|
56
190
|
/** Extra headers, applied last so they can override the ones above. */
|
|
57
191
|
headers?: Readonly<Record<string, string>>;
|
|
58
192
|
/**
|
|
@@ -69,13 +203,62 @@ export interface FoirClientConfig {
|
|
|
69
203
|
/**
|
|
70
204
|
* `foirSchemaVersion` from the generated module. When set, the client
|
|
71
205
|
* compares it against the server's manifest once and warns on mismatch.
|
|
72
|
-
*
|
|
206
|
+
* Carried by `generated`; set it here only to override that.
|
|
73
207
|
*/
|
|
74
208
|
schemaVersion?: number;
|
|
75
209
|
/** Set `false` to suppress the drift check even when `schemaVersion` is set. */
|
|
76
210
|
checkSchemaDrift?: boolean;
|
|
77
211
|
/** Called instead of `console.warn` when drift is detected. */
|
|
78
212
|
onSchemaDrift?: (drift: FoirSchemaDrift) => void;
|
|
213
|
+
/**
|
|
214
|
+
* Set `false` to silence the one-line notice a client prints when it is built
|
|
215
|
+
* with neither `generated` nor `selections` and will therefore read its
|
|
216
|
+
* selection trees from `GET /v1/manifest`.
|
|
217
|
+
*
|
|
218
|
+
* The notice exists because the fallback is legitimate and expensive at the
|
|
219
|
+
* same time: it is what makes a zero-codegen start work, and it is also what a
|
|
220
|
+
* mis-wired generated module used to silently become. Announcing it is the
|
|
221
|
+
* difference between choosing it and landing in it.
|
|
222
|
+
*/
|
|
223
|
+
announceManifestFallback?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* WebSocket endpoint. Defaults to `apiUrl` with the scheme swapped to `ws`
|
|
226
|
+
* and `/graphql/ws` appended, which is where the public API serves
|
|
227
|
+
* subscriptions. Override only for a proxy or a test server.
|
|
228
|
+
*/
|
|
229
|
+
wsUrl?: string;
|
|
230
|
+
/**
|
|
231
|
+
* WebSocket constructor, called as `(url, protocols)`.
|
|
232
|
+
*
|
|
233
|
+
* Defaults to `globalThis.WebSocket`, which browsers, Deno, Bun, Cloudflare
|
|
234
|
+
* Workers and Node 22+ all provide. On Node 18/20 there is none, so pass one:
|
|
235
|
+
* `webSocketImpl: (url, p) => new WebSocket(url, p)` from the `ws` package.
|
|
236
|
+
*/
|
|
237
|
+
webSocketImpl?: FoirWebSocketFactory;
|
|
238
|
+
/** How the subscription socket behaves when it drops. */
|
|
239
|
+
reconnect?: FoirReconnectPolicy;
|
|
240
|
+
/**
|
|
241
|
+
* Extra `connection_init` parameters, resolved on EVERY connect.
|
|
242
|
+
*
|
|
243
|
+
* A WebSocket handshake carries no headers a browser will let you set, so
|
|
244
|
+
* graphql-ws puts credentials in the `connection_init` payload — and this
|
|
245
|
+
* client builds that payload from the same `buildFoirHeaders` the HTTP
|
|
246
|
+
* transport uses, so `apiKey`, `bearerToken` and `headers` all reach the
|
|
247
|
+
* socket already. What none of them can do is carry a credential that is
|
|
248
|
+
* neither of the two the header builder knows about, resolved fresh.
|
|
249
|
+
*
|
|
250
|
+
* Both halves matter. `headers` is a static object captured when the client
|
|
251
|
+
* was constructed; a socket outlives a short-lived token, and a reconnect
|
|
252
|
+
* that replays a captured credential re-presents an expired one and fails to
|
|
253
|
+
* re-authenticate — on exactly the drop the reconnect logic exists to
|
|
254
|
+
* recover from. This is a function, called per connect, so the credential
|
|
255
|
+
* the socket presents is the one that is valid now.
|
|
256
|
+
*
|
|
257
|
+
* Merged LAST, so it wins over everything above it. Returning `{}` (or
|
|
258
|
+
* omitting the field) leaves the payload exactly as the header builder made
|
|
259
|
+
* it.
|
|
260
|
+
*/
|
|
261
|
+
connectionParams?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
79
262
|
}
|
|
80
263
|
export interface FoirSchemaDrift {
|
|
81
264
|
/** Manifest version the local types were generated from. */
|
|
@@ -87,21 +270,56 @@ export interface FoirSchemaDrift {
|
|
|
87
270
|
/**
|
|
88
271
|
* Build a `/v1/...` URL from whatever the caller put in `apiUrl`.
|
|
89
272
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
273
|
+
* The published name for `foirApiEndpoint`, which is where the implementation
|
|
274
|
+
* lives now that `@foir/sdk/server`'s events client needs the same tolerance for
|
|
275
|
+
* a `PLATFORM_API_URL` ending in `/graphql`. Kept as a named function rather
|
|
276
|
+
* than a re-export so `@foir/sdk/data`'s public surface is unchanged.
|
|
93
277
|
*/
|
|
94
278
|
export declare function foirDataEndpoint(apiUrl: string, path: `/${string}`): string;
|
|
95
279
|
/** Resolve `config.bearerToken`, whether it is a string, a function, or absent. */
|
|
96
280
|
export declare function resolveBearerToken(config: Pick<FoirClientConfig, 'bearerToken'>): Promise<string | undefined>;
|
|
281
|
+
/** Which GraphQL endpoint a call is sent to. */
|
|
282
|
+
export type FoirLane = 'shared' | 'origin';
|
|
283
|
+
/**
|
|
284
|
+
* Why a call did not take the shared lane. One name per condition the server's
|
|
285
|
+
* gate refuses on, so a decision can be asserted rather than inferred.
|
|
286
|
+
*/
|
|
287
|
+
export type FoirLaneReason = 'key' | 'bearer' | 'operation' | 'channel';
|
|
288
|
+
export interface FoirRouteDecision {
|
|
289
|
+
lane: FoirLane;
|
|
290
|
+
/** Set only on the origin lane: the first condition that ruled the shared one out. */
|
|
291
|
+
reason?: FoirLaneReason;
|
|
292
|
+
}
|
|
293
|
+
export interface FoirRouteInput {
|
|
294
|
+
apiKey: string;
|
|
295
|
+
/** The RESOLVED bearer, not the provider — a provider that yields null is not a bearer. */
|
|
296
|
+
bearerToken?: string;
|
|
297
|
+
mutation: boolean;
|
|
298
|
+
channel?: 'published' | 'draft';
|
|
299
|
+
}
|
|
97
300
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* The
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
301
|
+
* Where this call goes.
|
|
302
|
+
*
|
|
303
|
+
* The first condition — that the credential is in the URL — needs no test here
|
|
304
|
+
* because this client always puts it there (`buildCdnUrl`); it is asserted on
|
|
305
|
+
* the built URL instead. The other four are decided below, in the order the
|
|
306
|
+
* server states them.
|
|
307
|
+
*/
|
|
308
|
+
export declare function foirRoute(input: FoirRouteInput): FoirRouteDecision;
|
|
309
|
+
/**
|
|
310
|
+
* Execute a printed document and return the single root field's value.
|
|
311
|
+
*
|
|
312
|
+
* One op prints exactly one root field, and the plan says which — so unlike the
|
|
313
|
+
* JSON lane, which had to guess from "the only key of `data`", this reads the
|
|
314
|
+
* name it asked for. A response missing it means the server answered something
|
|
315
|
+
* else, and saying so beats returning `undefined`.
|
|
316
|
+
*/
|
|
317
|
+
export declare function executeFoirDocument<T>(config: FoirClientConfig, plan: FoirDocumentPlan): Promise<T>;
|
|
318
|
+
/**
|
|
319
|
+
* Execute a hand-written document and return the whole `data` object.
|
|
320
|
+
*
|
|
321
|
+
* The escape hatch keeps the routing, the APQ handshake and the error shape;
|
|
322
|
+
* what it gives up is the single-root-field unwrapping, because a document
|
|
323
|
+
* written by hand may legitimately have several.
|
|
106
324
|
*/
|
|
107
|
-
export declare function
|
|
325
|
+
export declare function executeFoirRawDocument<T>(config: FoirClientConfig, document: string, mutation: boolean, variables?: Record<string, unknown>): Promise<T>;
|