@foir/sdk 0.7.0 → 0.8.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 +805 -4
- package/README.md +46 -15
- package/dist/auth/auth.d.ts +104 -1
- package/dist/auth/index.d.ts +12 -6
- package/dist/auth/operations.d.ts +85 -64
- package/dist/auth/ops.d.ts +99 -0
- package/dist/auth.js +775 -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 +302 -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 +266 -3
- 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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subject registry — which records this client fetched.
|
|
3
|
+
*
|
|
4
|
+
* ---------------------------------------------------------------------------
|
|
5
|
+
* What it is for
|
|
6
|
+
* ---------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* An annotation surface — a comment widget, a feedback button, a review prompt
|
|
9
|
+
* — has to say what it is about. Every tool that does this anchors to a URL or
|
|
10
|
+
* to a CSS selector, and both break the moment the page is redesigned, the
|
|
11
|
+
* route changes, or the reader is served a different locale or variant.
|
|
12
|
+
*
|
|
13
|
+
* A page rendered through this client does not have that problem, because the
|
|
14
|
+
* client already knows: it printed the document, it named the model, and every
|
|
15
|
+
* record in the response carries `_id` (the selection model protects it — see
|
|
16
|
+
* `PROTECTED_ROOT_FIELD`). So the anchor can be the RECORD, which survives all
|
|
17
|
+
* three, and it can be derived rather than configured.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const subjects = createFoirSubjectRegistry();
|
|
21
|
+
* const foir = createFoirClient({ apiUrl, apiKey, generated, subjects });
|
|
22
|
+
*
|
|
23
|
+
* await foir.article.findUnique({ naturalKey: 'about-us' });
|
|
24
|
+
* subjects.subject(); // { modelKey: 'article', recordId: 'kx7p…' }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Nothing here maps a URL, and nothing here may start to. A registry that fell
|
|
28
|
+
* back to the address bar would reintroduce exactly the fragility the record
|
|
29
|
+
* anchor exists to remove, and it would do so silently — the anchor would look
|
|
30
|
+
* identical and would point somewhere else after the next redesign.
|
|
31
|
+
*
|
|
32
|
+
* ---------------------------------------------------------------------------
|
|
33
|
+
* Why it is opt-in, and off unless you build one
|
|
34
|
+
* ---------------------------------------------------------------------------
|
|
35
|
+
*
|
|
36
|
+
* A list of the records a page fetched is a DISCLOSURE SURFACE. It names
|
|
37
|
+
* content by model key and id, including records a visitor never saw rendered —
|
|
38
|
+
* a draft pulled for an editor, a related record fetched and then filtered out
|
|
39
|
+
* in the template, the twelve nav pages behind a menu. Handing that to whatever
|
|
40
|
+
* else runs on the page (an analytics tag, an embedded widget, a browser
|
|
41
|
+
* extension) is a decision with consequences, and it is not this package's
|
|
42
|
+
* decision to make.
|
|
43
|
+
*
|
|
44
|
+
* So it is not a flag that defaults to false — it is an OBJECT the host
|
|
45
|
+
* constructs and owns. There is no configuration that turns it on by accident,
|
|
46
|
+
* the host holds the only reference, and its lifetime is the host's: a registry
|
|
47
|
+
* built per request on an edge runtime cannot outlive that request, which is the
|
|
48
|
+
* same reason nothing in this client keeps module-level state.
|
|
49
|
+
*
|
|
50
|
+
* The corollary is the one warning worth repeating: a registry built at MODULE
|
|
51
|
+
* scope on a multi-tenant edge worker is shared across tenants, exactly like a
|
|
52
|
+
* module-level response cache would be. Build one per client.
|
|
53
|
+
*
|
|
54
|
+
* ---------------------------------------------------------------------------
|
|
55
|
+
* Where the subjects come from
|
|
56
|
+
* ---------------------------------------------------------------------------
|
|
57
|
+
*
|
|
58
|
+
* From the plan, not from the response's shape. `planCall` derives the record
|
|
59
|
+
* positions from the payload the manifest published (`FoirDocumentPlan.records`)
|
|
60
|
+
* — the same walk that decides where `pick`/`omit` land — so the registry reads
|
|
61
|
+
* the response at the positions the server said records would be, rather than
|
|
62
|
+
* hunting for objects that happen to carry an `_id`.
|
|
63
|
+
*/
|
|
64
|
+
import { type FoirDocumentPlan } from './document';
|
|
65
|
+
/**
|
|
66
|
+
* One thing an annotation can be about.
|
|
67
|
+
*
|
|
68
|
+
* `recordId` / `modelKey` and not `id` / `model`, matching
|
|
69
|
+
* `FoirRecordChangedEvent` in `subscriptions.ts`: a subscription event and a
|
|
70
|
+
* fetched subject name the same pair, and two spellings of one pair is how a
|
|
71
|
+
* consumer ends up writing an adapter between two halves of one SDK.
|
|
72
|
+
*/
|
|
73
|
+
export interface FoirSubject {
|
|
74
|
+
/** The record's model key, e.g. `article`. */
|
|
75
|
+
modelKey: string;
|
|
76
|
+
/** The record's `_id`. */
|
|
77
|
+
recordId: string;
|
|
78
|
+
/**
|
|
79
|
+
* A human label for the record, when one is available. See
|
|
80
|
+
* `FoirSubjectOptions.label` for where it comes from and why the client does
|
|
81
|
+
* not pick a field on its own.
|
|
82
|
+
*/
|
|
83
|
+
label?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* How a record reached the registry.
|
|
87
|
+
*
|
|
88
|
+
* NOT a heuristic: it is read off the op's published payload. A payload whose
|
|
89
|
+
* kind is `record` IS one record, which means the caller addressed one record by
|
|
90
|
+
* name (`findUnique`, `create`, a lookup); a record reached through a field of
|
|
91
|
+
* an `object` payload arrived inside something — a connection's `edges.node`, a
|
|
92
|
+
* search wrapper's `record`.
|
|
93
|
+
*
|
|
94
|
+
* That distinction is what lets `subject()` answer at all. A page renders FROM
|
|
95
|
+
* the record it addressed and merely LISTS the rest, and the manifest already
|
|
96
|
+
* says which is which, so the client does not have to guess from result counts
|
|
97
|
+
* or field names.
|
|
98
|
+
*/
|
|
99
|
+
export type FoirSubjectOrigin = 'record' | 'collection';
|
|
100
|
+
/** What `FoirSubjectRegistry.add` takes. */
|
|
101
|
+
export interface FoirSubjectEntry {
|
|
102
|
+
modelKey: string;
|
|
103
|
+
recordId: string;
|
|
104
|
+
/**
|
|
105
|
+
* Defaults to `record`, which is what a hand-seeded entry means: you named
|
|
106
|
+
* this record deliberately. The data path passes the origin it read off the
|
|
107
|
+
* plan.
|
|
108
|
+
*/
|
|
109
|
+
origin?: FoirSubjectOrigin;
|
|
110
|
+
/**
|
|
111
|
+
* The record as it arrived, for the labeller to read. Optional, because a
|
|
112
|
+
* host seeding a subject from an id has no record object to hand.
|
|
113
|
+
*/
|
|
114
|
+
record?: Readonly<Record<string, unknown>>;
|
|
115
|
+
/** An explicit label. Wins over the labeller and over `_naturalKey`. */
|
|
116
|
+
label?: string;
|
|
117
|
+
}
|
|
118
|
+
export interface FoirSubjectOptions {
|
|
119
|
+
/**
|
|
120
|
+
* Turn a record into the label a widget shows: `"About Us"` rather than
|
|
121
|
+
* `article/kx7p…`.
|
|
122
|
+
*
|
|
123
|
+
* There is no default that reads one of your fields, and that is deliberate.
|
|
124
|
+
* The manifest publishes a model's scalars and their types; it does not
|
|
125
|
+
* publish which one is the title, because there is no such concept — one
|
|
126
|
+
* project calls it `title`, the next `name`, the next `headline`. A client
|
|
127
|
+
* that picked `title` when it saw one would label most projects correctly and
|
|
128
|
+
* mislabel the rest, silently, in the one string a reader actually reads.
|
|
129
|
+
*
|
|
130
|
+
* So the field is yours to name, in one line:
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* createFoirSubjectRegistry({
|
|
134
|
+
* label: (record) => (record.title as string) ?? null,
|
|
135
|
+
* });
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* Return `null` or `undefined` for "no label" — the subject is still
|
|
139
|
+
* recorded, just without one. It must not throw: see `recordFoirSubjects`.
|
|
140
|
+
*/
|
|
141
|
+
label?: (record: Readonly<Record<string, unknown>>, subject: FoirSubject) => string | null | undefined;
|
|
142
|
+
}
|
|
143
|
+
export interface FoirSubjectRegistry {
|
|
144
|
+
/**
|
|
145
|
+
* Record one subject.
|
|
146
|
+
*
|
|
147
|
+
* The data path calls this for you. Call it yourself to seed a subject this
|
|
148
|
+
* client did not fetch — a server-rendered page whose record was read on the
|
|
149
|
+
* server, or a record that arrived through `foir.raw()`, which prints no
|
|
150
|
+
* published payload and therefore has no record positions to read.
|
|
151
|
+
*/
|
|
152
|
+
add(entry: FoirSubjectEntry): void;
|
|
153
|
+
/**
|
|
154
|
+
* Every record recorded so far, de-duplicated on `modelKey` + `recordId` and
|
|
155
|
+
* ordered least-recently-fetched first — so the last entry is the last record
|
|
156
|
+
* this client saw, whether or not it was addressed by name.
|
|
157
|
+
*
|
|
158
|
+
* This is what a widget offers when it wants to let the reader choose, and it
|
|
159
|
+
* is the honest answer for a page whose data all came out of lists.
|
|
160
|
+
*/
|
|
161
|
+
fetched(): readonly FoirSubject[];
|
|
162
|
+
/**
|
|
163
|
+
* The subject to annotate: your override if you set one, otherwise the last
|
|
164
|
+
* record this client addressed BY NAME.
|
|
165
|
+
*
|
|
166
|
+
* One rule, and no case analysis behind it. A record that only ever appeared
|
|
167
|
+
* inside a list is never promoted — a page that fetched twelve nav entries and
|
|
168
|
+
* nothing else has no subject, and saying so is better than picking one of the
|
|
169
|
+
* twelve. Use `fetched()` or `setSubject()` there.
|
|
170
|
+
*/
|
|
171
|
+
subject(): FoirSubject | undefined;
|
|
172
|
+
/**
|
|
173
|
+
* Pin the subject, overriding whatever was fetched. `null` drops the override
|
|
174
|
+
* and goes back to the fetched answer.
|
|
175
|
+
*/
|
|
176
|
+
setSubject(subject: FoirSubject | null): void;
|
|
177
|
+
/**
|
|
178
|
+
* Forget everything: the recorded records AND the override.
|
|
179
|
+
*
|
|
180
|
+
* A single-page app must call this when it navigates. Without it the registry
|
|
181
|
+
* accumulates every record the session ever touched and `subject()` keeps
|
|
182
|
+
* answering with a record from a page the reader has left.
|
|
183
|
+
*/
|
|
184
|
+
clear(): void;
|
|
185
|
+
/**
|
|
186
|
+
* Run `listener` after every change, and return the function that stops it.
|
|
187
|
+
*
|
|
188
|
+
* For a widget mounted before the data resolves. Listeners run synchronously
|
|
189
|
+
* in registration order; one that throws does not stop the others, and the
|
|
190
|
+
* failure is reported rather than dropped (see `recordFoirSubjects`).
|
|
191
|
+
*/
|
|
192
|
+
subscribe(listener: () => void): () => void;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Build a registry.
|
|
196
|
+
*
|
|
197
|
+
* Per client, never at module scope — see the file header. It is handed to
|
|
198
|
+
* `createFoirClient` as `subjects`, and the host keeps the same reference to
|
|
199
|
+
* read from.
|
|
200
|
+
*/
|
|
201
|
+
export declare function createFoirSubjectRegistry(options?: FoirSubjectOptions): FoirSubjectRegistry;
|
|
202
|
+
/**
|
|
203
|
+
* Record what one call fetched.
|
|
204
|
+
*
|
|
205
|
+
* A no-op without a registry, which is what "off by default" means at runtime:
|
|
206
|
+
* a client built without `subjects` walks nothing and allocates nothing.
|
|
207
|
+
*
|
|
208
|
+
* # Errors are not swallowed here
|
|
209
|
+
*
|
|
210
|
+
* The only things that can fail are the host's own: a `label` function, or a
|
|
211
|
+
* `subscribe` listener. Both are re-thrown, wrapped in a message that says the
|
|
212
|
+
* REQUEST succeeded so the failure cannot be read as a transport fault.
|
|
213
|
+
*
|
|
214
|
+
* Catching them would be worse than it looks. A subject that silently failed to
|
|
215
|
+
* record does not degrade to "no annotation offered" — it degrades to the
|
|
216
|
+
* PREVIOUS subject still being the answer, so the widget offers to comment on
|
|
217
|
+
* the record the reader was looking at a moment ago. A loud failure in a
|
|
218
|
+
* function the host wrote is strictly better than an annotation filed against
|
|
219
|
+
* the wrong record.
|
|
220
|
+
*/
|
|
221
|
+
export declare function recordFoirSubjects(registry: FoirSubjectRegistry | undefined, plan: Pick<FoirDocumentPlan, 'records'>, result: unknown): void;
|
|
@@ -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
|
+
}
|