@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +805 -4
  2. package/README.md +46 -15
  3. package/dist/auth/auth.d.ts +104 -1
  4. package/dist/auth/index.d.ts +12 -6
  5. package/dist/auth/operations.d.ts +85 -64
  6. package/dist/auth/ops.d.ts +99 -0
  7. package/dist/auth.js +775 -106
  8. package/dist/chunk-IGNLWT3H.js +35 -0
  9. package/dist/chunk-OHYVKTGB.js +45 -0
  10. package/dist/chunk-TAL5H6VA.js +39 -0
  11. package/dist/client.js +25 -31
  12. package/dist/codegen/block-registry.d.ts +13 -19
  13. package/dist/codegen/block-registry.js +3 -31
  14. package/dist/codegen/block-scaffold.d.ts +12 -21
  15. package/dist/codegen/block-scaffold.js +3 -41
  16. package/dist/codegen/index.d.ts +9 -4
  17. package/dist/codegen/index.js +7 -11
  18. package/dist/codegen/item-registry.d.ts +28 -0
  19. package/dist/codegen/item-registry.js +7 -0
  20. package/dist/codegen/item-scaffold.d.ts +29 -0
  21. package/dist/codegen/item-scaffold.js +7 -0
  22. package/dist/codegen/preset.d.ts +14 -7
  23. package/dist/codegen/shared.d.ts +20 -9
  24. package/dist/content/content-renderer.d.ts +90 -24
  25. package/dist/content/index.d.ts +13 -7
  26. package/dist/content/richtext.d.ts +66 -18
  27. package/dist/content.js +227 -80
  28. package/dist/data/builtins.d.ts +302 -92
  29. package/dist/data/client.d.ts +97 -25
  30. package/dist/data/document.d.ts +251 -0
  31. package/dist/data/index.d.ts +38 -19
  32. package/dist/data/manifest.d.ts +266 -3
  33. package/dist/data/schema.d.ts +78 -8
  34. package/dist/data/selection.d.ts +231 -37
  35. package/dist/data/subjects.d.ts +221 -0
  36. package/dist/data/subscription-document.d.ts +75 -0
  37. package/dist/data/subscriptions.d.ts +181 -0
  38. package/dist/data/transport.d.ts +255 -37
  39. package/dist/data/ws-transport.d.ts +197 -0
  40. package/dist/data.js +1419 -45
  41. package/dist/graphql/cdn-route.d.ts +12 -28
  42. package/dist/media/index.d.ts +6 -2
  43. package/dist/media/media.d.ts +2 -2
  44. package/dist/media/screen-capture.d.ts +81 -0
  45. package/dist/media.js +97 -0
  46. package/dist/next.js +663 -182
  47. package/dist/push.js +700 -90
  48. package/dist/react/FoirFeedback.d.ts +62 -0
  49. package/dist/react/feedback-notes.d.ts +101 -0
  50. package/dist/react/index.d.ts +7 -1
  51. package/dist/react.js +1272 -194
  52. package/dist/realtime.js +1 -1
  53. package/dist/server/events-client.d.ts +279 -0
  54. package/dist/server/index.d.ts +4 -0
  55. package/dist/server.js +298 -0
  56. package/dist/transport/foir-http.d.ts +23 -6
  57. package/dist/transport/persisted-query.d.ts +68 -0
  58. package/dist/transport/retry.d.ts +1 -1
  59. package/package.json +13 -1
  60. 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
+ }