@foir/sdk 0.30.0 → 0.31.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 +14 -0
- package/README.md +1 -1
- package/dist/ai/index.d.ts +10 -15
- package/dist/ai.js +185 -8
- package/dist/app/client.d.ts +9 -5
- package/dist/app.js +12 -13
- package/dist/auth/auth.d.ts +80 -101
- package/dist/auth/hosted.d.ts +12 -12
- package/dist/auth/index.d.ts +11 -7
- package/dist/auth/operations.d.ts +17 -32
- package/dist/auth/ops.d.ts +4 -13
- package/dist/auth/session-client.d.ts +4 -4
- package/dist/auth/steps.d.ts +50 -0
- package/dist/auth-session.js +3 -3
- package/dist/auth.js +157 -75
- package/dist/content/content-renderer.d.ts +2 -2
- package/dist/content/richtext.d.ts +2 -2
- package/dist/content.js +7 -7
- package/dist/data/builtins.d.ts +64 -33
- package/dist/data/index.d.ts +2 -2
- package/dist/data/manifest.d.ts +1 -1
- package/dist/data/schema.d.ts +1 -1
- package/dist/data/selection.d.ts +3 -3
- package/dist/data.js +5 -2
- package/dist/next/index.d.ts +1 -1
- package/dist/next/session.d.ts +27 -11
- package/dist/next.js +98 -40
- package/dist/push.js +9 -19
- package/dist/react/FoirProvider.d.ts +18 -16
- package/dist/react/feedback-notes.d.ts +1 -2
- package/dist/react.js +167 -84
- package/dist/runs/step-reply.d.ts +81 -0
- package/dist/server/callback-client.d.ts +11 -16
- package/dist/server/index.d.ts +2 -0
- package/dist/server/operation-types.d.ts +6 -13
- package/dist/server.js +76 -107
- package/dist/transport/foir-http.d.ts +20 -6
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -180,6 +180,20 @@ Changes on `dev` that have not been published to npm.
|
|
|
180
180
|
stream nobody read, which threw. A page loaded again still picks the answer
|
|
181
181
|
up from the stored run with `resume`.
|
|
182
182
|
|
|
183
|
+
### Changed
|
|
184
|
+
|
|
185
|
+
- **Breaking: a prose entry's identity is `_id`.** `Passage._key` is now
|
|
186
|
+
`Passage._id`, a markDef is named by its `_id`, and the renderers key
|
|
187
|
+
entries by `_id` alone. Ask for `_id` where a query asked for `_key`. The id
|
|
188
|
+
is minted once and never changes, and is unique within its record; it is not
|
|
189
|
+
a record id.
|
|
190
|
+
- **Breaking: a reference is followed by `_targetId` alone.**
|
|
191
|
+
`Reference._targetNaturalKey` is gone: a reference names its target by id.
|
|
192
|
+
`Reference._id` is the entry's own id, never the target's.
|
|
193
|
+
- **`Image`, `Video`, `FileValue` and `GeoPoint` carry `_id`**: the value's own
|
|
194
|
+
id when it is an item of a list or an entry of a prose field, and null on a
|
|
195
|
+
plain field's value.
|
|
196
|
+
|
|
183
197
|
## [0.29.0] — 2026-10-21
|
|
184
198
|
|
|
185
199
|
### Added
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ pnpm add yjs y-websocket # for @foir/sdk/realtime
|
|
|
27
27
|
| `@foir/sdk/data` | Typed data client over `/graphql` — `createFoirClient`, per-key-shaped ops, `include`/`omit`, `fetchFoirManifest`; prints the document for you, so still no `graphql` | **none** |
|
|
28
28
|
| `@foir/sdk/auth` | Framework-agnostic sign-in for your app's users: login/register/OTP/provider flows, token lifecycle (`SessionAdapter`), `createFoirSessionClient` (Foir-hosted cookie sessions), `authedRequest`, guards | `graphql` |
|
|
29
29
|
| `@foir/sdk/react` | Drop-in React auth — `<FoirProvider>` (token or `hostedSession` mode), `useFoirSession`/`useFoirUser`/`useFoirAuth`, `<SignIn>`, `<OAuthButtons>`, `<UserButton>` | `react`, `graphql` |
|
|
30
|
-
| `@foir/sdk/next` | Next.js App Router BFF auth
|
|
30
|
+
| `@foir/sdk/next` | Next.js App Router BFF auth: `createFoirCookieAdapter`, `getFoirSession`, `foirSignIn*`, `startFoirProviderSignIn`, `finishFoirProviderSignIn`, `createFoirMiddleware` | `graphql` |
|
|
31
31
|
| `@foir/sdk/server` | Server-side helpers for extension/config API backends — `createServerClient`, `verifyWebhookSignature`, `verifyScopedToken`, callback client | — |
|
|
32
32
|
| `@foir/sdk/live` | Live reads — the client half of Foir's invalidation graph: tag index, raw-payload store, `seq` ordering, record-change → tag mapping | **none** |
|
|
33
33
|
| `@foir/sdk/live/react` | `<FoirLive>` — one socket, refcounted per model, wired to a query client you already have | `react` |
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -55,9 +55,10 @@
|
|
|
55
55
|
* `foirChatMessages` reads a thread back as `useChat`'s messages.
|
|
56
56
|
*/
|
|
57
57
|
import type { ChatTransport, FlexibleSchema, UIMessage, UIMessageChunk } from 'ai';
|
|
58
|
-
import type { FoirAgentAttachmentUpload, FoirAgentRun, FoirAgentRunNode, FoirAgentThread, FoirCreateAgentAttachmentArgs, FoirStartAgentRunArgs } from '../data/builtins';
|
|
58
|
+
import type { FoirAgentAttachmentUpload, FoirAgentRun, FoirAgentHeldAct, FoirAgentRunNode, FoirAgentThread, FoirCreateAgentAttachmentArgs, FoirStartAgentRunArgs } from '../data/builtins';
|
|
59
59
|
import type { FoirAgentRunEvent, FoirAgentRunSubscribeOptions } from '../data/subscriptions';
|
|
60
60
|
import type { FoirSubscription } from '../data/ws-transport';
|
|
61
|
+
import { type StepReply } from '../runs/step-reply';
|
|
61
62
|
/** What the transport needs of a client: the agents namespace a key that runs an agent is given. */
|
|
62
63
|
export interface FoirChatClient {
|
|
63
64
|
ns: {
|
|
@@ -68,12 +69,7 @@ export interface FoirChatClient {
|
|
|
68
69
|
id: string;
|
|
69
70
|
approve: boolean;
|
|
70
71
|
reason?: string;
|
|
71
|
-
}): Promise<
|
|
72
|
-
answerPageTool(args: {
|
|
73
|
-
id: string;
|
|
74
|
-
output: unknown;
|
|
75
|
-
visitorToken?: string;
|
|
76
|
-
}): Promise<FoirAgentRunNode | null>;
|
|
72
|
+
}): Promise<FoirAgentHeldAct | null>;
|
|
77
73
|
agentThread(args: {
|
|
78
74
|
id: string;
|
|
79
75
|
visitorToken?: string;
|
|
@@ -133,12 +129,6 @@ export interface FoirChatTransportOptions {
|
|
|
133
129
|
*/
|
|
134
130
|
pageTools?: Record<string, FoirPageTool>;
|
|
135
131
|
}
|
|
136
|
-
/**
|
|
137
|
-
* One run's events as `useChat`'s chunks. A run outlives a stream: it stops
|
|
138
|
-
* for a decision and carries on once it is made, so what the person has
|
|
139
|
-
* already been shown (the steps, the last event) is kept per run, and each
|
|
140
|
-
* stream only opens and closes its own text.
|
|
141
|
-
*/
|
|
142
132
|
export declare class FoirRunChunks {
|
|
143
133
|
private readonly runId;
|
|
144
134
|
private readonly runs;
|
|
@@ -150,14 +140,19 @@ export declare class FoirRunChunks {
|
|
|
150
140
|
private opened;
|
|
151
141
|
/** The run stopped for this stream's followers: it ended, or holds an act. */
|
|
152
142
|
stopped: boolean;
|
|
153
|
-
/**
|
|
154
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Calls to the page's tools waiting for the app's `addToolOutput`, each with
|
|
145
|
+
* the reply the page answers it by.
|
|
146
|
+
*/
|
|
147
|
+
readonly pageCalls: Map<string, StepReply | null>;
|
|
155
148
|
/** Calls to the page's tools the transport runs and answers itself, until the run hears them. */
|
|
156
149
|
readonly answering: Set<string>;
|
|
157
150
|
/** Calls the transport is to run: taken with `takeCalls`. */
|
|
158
151
|
private toRun;
|
|
159
152
|
/** Acts held for a decision, by anyone. */
|
|
160
153
|
private readonly held;
|
|
154
|
+
/** The messages whose text this run has shown, streamed or whole. */
|
|
155
|
+
private readonly shown;
|
|
161
156
|
/** `runs` names the page's tools the transport runs itself. */
|
|
162
157
|
constructor(runId: string, runs?: (tool: string) => boolean);
|
|
163
158
|
/** The calls to the page's tools that arrived for the transport to run. */
|
package/dist/ai.js
CHANGED
|
@@ -57,9 +57,162 @@ function randomSecret() {
|
|
|
57
57
|
return out;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// src/transport/foir-http.ts
|
|
61
|
+
var FoirRequestError = class extends Error {
|
|
62
|
+
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
63
|
+
status;
|
|
64
|
+
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
65
|
+
graphQLErrors;
|
|
66
|
+
/**
|
|
67
|
+
* The `data` the server did return, when a STRICT read refused a partial
|
|
68
|
+
* response. Present only on that path: without it, opting into strictness
|
|
69
|
+
* would trade a silent wrong answer for a thrown one that discards the answer,
|
|
70
|
+
* and a caller that wants to log what it nearly rendered could not.
|
|
71
|
+
*/
|
|
72
|
+
partialData;
|
|
73
|
+
/**
|
|
74
|
+
* The selections the server stripped, when the refusal was a pruned document
|
|
75
|
+
* rather than a partial error. Same purpose as `partialData`: a caller that
|
|
76
|
+
* wants to log what it nearly rendered, or name the offending path in a
|
|
77
|
+
* developer-facing message, has it without re-parsing anything.
|
|
78
|
+
*/
|
|
79
|
+
prunedSelections;
|
|
80
|
+
cause;
|
|
81
|
+
constructor(message, options) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = "FoirRequestError";
|
|
84
|
+
this.cause = options?.cause;
|
|
85
|
+
this.status = options?.status;
|
|
86
|
+
this.graphQLErrors = options?.graphQLErrors;
|
|
87
|
+
this.partialData = options?.partialData;
|
|
88
|
+
this.prunedSelections = options?.prunedSelections;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The code of the first error, if the response carried one.
|
|
92
|
+
*
|
|
93
|
+
* A getter rather than a stored field so it stays correct for an error
|
|
94
|
+
* constructed anywhere, including by a caller in a test.
|
|
95
|
+
*/
|
|
96
|
+
get code() {
|
|
97
|
+
return this.graphQLErrors?.[0]?.extensions?.code;
|
|
98
|
+
}
|
|
99
|
+
/** The correlation id, if the response carried one. */
|
|
100
|
+
get requestId() {
|
|
101
|
+
return this.graphQLErrors?.[0]?.extensions?.requestId;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether retrying this request could succeed.
|
|
105
|
+
*
|
|
106
|
+
* The 429 is the one that has to be in here and is easy to leave out: it sits
|
|
107
|
+
* among the 4xx statuses that are otherwise permanent, so a policy written as
|
|
108
|
+
* "retry 5xx, never retry 4xx" excludes exactly the failure that clears on its
|
|
109
|
+
* own. A storefront wrote that policy, its key saturated, and every read
|
|
110
|
+
* failed on the first attempt for an hour.
|
|
111
|
+
*/
|
|
112
|
+
get retryable() {
|
|
113
|
+
if (this.status === void 0) {
|
|
114
|
+
if (this.graphQLErrors?.length || this.prunedSelections?.length) {
|
|
115
|
+
return this.code === "RATE_LIMITED" || this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (this.status === 429 || this.code === "RATE_LIMITED") return true;
|
|
120
|
+
if (this.status >= 500) return true;
|
|
121
|
+
return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* How long to wait before retrying, in milliseconds, when the server said.
|
|
125
|
+
* Undefined when it did not, which means the caller picks its own backoff.
|
|
126
|
+
*/
|
|
127
|
+
get retryAfterMs() {
|
|
128
|
+
const seconds = this.graphQLErrors?.[0]?.extensions?.retryAfter;
|
|
129
|
+
return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/transport/retry.ts
|
|
134
|
+
var DEFAULT_ATTEMPTS = 3;
|
|
135
|
+
var DEFAULT_BASE_DELAY_MS = 200;
|
|
136
|
+
var DEFAULT_MAX_DELAY_MS = 2e4;
|
|
137
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
138
|
+
function foirRetryDelayMs(error, attempt, policy = {}) {
|
|
139
|
+
if (!(error instanceof FoirRequestError) || !error.retryable) return null;
|
|
140
|
+
const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
141
|
+
const stated = error.retryAfterMs;
|
|
142
|
+
if (stated !== void 0) return Math.min(stated, max);
|
|
143
|
+
const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
144
|
+
return Math.min(base * 2 ** (attempt - 1), max);
|
|
145
|
+
}
|
|
146
|
+
async function withFoirRetry(fn, policy = {}) {
|
|
147
|
+
const attempts = Math.max(1, policy.attempts ?? DEFAULT_ATTEMPTS);
|
|
148
|
+
const sleep = policy.sleep ?? defaultSleep;
|
|
149
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
150
|
+
try {
|
|
151
|
+
return await fn();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const delayMs = attempt >= attempts ? null : foirRetryDelayMs(error, attempt, policy);
|
|
154
|
+
if (delayMs === null) throw error;
|
|
155
|
+
policy.onRetry?.({ attempt, delayMs, error });
|
|
156
|
+
await sleep(delayMs);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/runs/step-reply.ts
|
|
162
|
+
async function replyToStep(reply, body, opts = {}) {
|
|
163
|
+
if (!reply?.token || !reply.signingKey || !reply.endpoint) {
|
|
164
|
+
throw new Error("replyToStep: the step was handed no reply, so it cannot answer");
|
|
165
|
+
}
|
|
166
|
+
return sendReply(reply.endpoint, reply.token, reply.signingKey, body, opts);
|
|
167
|
+
}
|
|
168
|
+
async function sendReply(endpoint, token, signingKey, body, opts = {}) {
|
|
169
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
170
|
+
if (!fetchImpl) throw new Error("replyToStep: no fetch available; pass opts.fetch on runtimes without a global fetch");
|
|
171
|
+
const payload = JSON.stringify(body);
|
|
172
|
+
const signature = await hmacSha256Hex(signingKey, payload);
|
|
173
|
+
return withFoirRetry(async () => {
|
|
174
|
+
let resp;
|
|
175
|
+
try {
|
|
176
|
+
resp = await fetchImpl(endpoint, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: {
|
|
179
|
+
"Content-Type": "application/json",
|
|
180
|
+
Authorization: `Bearer ${token}`,
|
|
181
|
+
"X-Foir-Signature": `sha256=${signature}`
|
|
182
|
+
},
|
|
183
|
+
body: payload,
|
|
184
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
185
|
+
});
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (opts.signal?.aborted) throw err;
|
|
188
|
+
throw new FoirRequestError("the reply got no answer", { cause: err });
|
|
189
|
+
}
|
|
190
|
+
if (!resp.ok) {
|
|
191
|
+
throw new FoirRequestError(`the reply was refused with HTTP ${resp.status}: ${await resp.text()}`, { status: resp.status });
|
|
192
|
+
}
|
|
193
|
+
const out = await resp.json();
|
|
194
|
+
if (!out || typeof out.status !== "string") {
|
|
195
|
+
throw new FoirRequestError("the reply was answered with nothing", { status: resp.status });
|
|
196
|
+
}
|
|
197
|
+
return { status: out.status, cancelled: out.cancelled === true, applied: out.applied === true };
|
|
198
|
+
}, opts.retry);
|
|
199
|
+
}
|
|
200
|
+
async function hmacSha256Hex(secret, body) {
|
|
201
|
+
const enc = new TextEncoder();
|
|
202
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
203
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(body));
|
|
204
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
205
|
+
}
|
|
206
|
+
|
|
60
207
|
// src/ai/index.ts
|
|
61
208
|
var TOOL_KINDS = /* @__PURE__ */ new Set(["VERB", "OPERATION", "MCP_TOOL", "AGENT", "PAGE_TOOL"]);
|
|
62
209
|
var STOPS = /* @__PURE__ */ new Set(["COMPLETED", "FAILED", "SKIPPED", "CANCELLED", "HELD"]);
|
|
210
|
+
function keptText(parts) {
|
|
211
|
+
return parts.map((p) => {
|
|
212
|
+
const part = p;
|
|
213
|
+
return part && part.type === "text" && typeof part.text === "string" ? part.text : "";
|
|
214
|
+
}).join("");
|
|
215
|
+
}
|
|
63
216
|
var FoirRunChunks = class {
|
|
64
217
|
/** `runs` names the page's tools the transport runs itself. */
|
|
65
218
|
constructor(runId, runs = () => false) {
|
|
@@ -76,14 +229,19 @@ var FoirRunChunks = class {
|
|
|
76
229
|
opened = false;
|
|
77
230
|
/** The run stopped for this stream's followers: it ended, or holds an act. */
|
|
78
231
|
stopped = false;
|
|
79
|
-
/**
|
|
80
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Calls to the page's tools waiting for the app's `addToolOutput`, each with
|
|
234
|
+
* the reply the page answers it by.
|
|
235
|
+
*/
|
|
236
|
+
pageCalls = /* @__PURE__ */ new Map();
|
|
81
237
|
/** Calls to the page's tools the transport runs and answers itself, until the run hears them. */
|
|
82
238
|
answering = /* @__PURE__ */ new Set();
|
|
83
239
|
/** Calls the transport is to run: taken with `takeCalls`. */
|
|
84
240
|
toRun = [];
|
|
85
241
|
/** Acts held for a decision, by anyone. */
|
|
86
242
|
held = /* @__PURE__ */ new Set();
|
|
243
|
+
/** The messages whose text this run has shown, streamed or whole. */
|
|
244
|
+
shown = /* @__PURE__ */ new Set();
|
|
87
245
|
/** The calls to the page's tools that arrived for the transport to run. */
|
|
88
246
|
takeCalls() {
|
|
89
247
|
const out = this.toRun;
|
|
@@ -118,6 +276,7 @@ var FoirRunChunks = class {
|
|
|
118
276
|
switch (event.kind) {
|
|
119
277
|
case "TEXT": {
|
|
120
278
|
if (!event.text) break;
|
|
279
|
+
if (event.messageId) this.shown.add(event.messageId);
|
|
121
280
|
this.open(out, fresh);
|
|
122
281
|
this.closeReasoning(out);
|
|
123
282
|
if (!this.textId) {
|
|
@@ -146,6 +305,19 @@ var FoirRunChunks = class {
|
|
|
146
305
|
out.push(...this.toolChunks(node));
|
|
147
306
|
break;
|
|
148
307
|
}
|
|
308
|
+
case "MESSAGE": {
|
|
309
|
+
const message = event.message;
|
|
310
|
+
if (!message || message.role !== "ASSISTANT" || this.shown.has(message.id)) break;
|
|
311
|
+
const text = keptText(message.parts);
|
|
312
|
+
if (!text) break;
|
|
313
|
+
this.shown.add(message.id);
|
|
314
|
+
this.open(out, fresh);
|
|
315
|
+
this.closeText(out);
|
|
316
|
+
this.closeReasoning(out);
|
|
317
|
+
const id = `${this.runId}:m:${message.id}`;
|
|
318
|
+
out.push({ type: "text-start", id }, { type: "text-delta", id, delta: text }, { type: "text-end", id });
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
149
321
|
case "DATA": {
|
|
150
322
|
if (!event.dataName) break;
|
|
151
323
|
this.open(out, fresh);
|
|
@@ -161,7 +333,7 @@ var FoirRunChunks = class {
|
|
|
161
333
|
if (event.status === "FAILED") {
|
|
162
334
|
out.push({ type: "error", errorText: event.error || "The answer could not be finished." });
|
|
163
335
|
}
|
|
164
|
-
if (event.status === "SKIPPED" && event.answer && this.tools.size === 0) {
|
|
336
|
+
if (event.status === "SKIPPED" && event.answer && this.tools.size === 0 && this.shown.size === 0) {
|
|
165
337
|
const id = `${this.runId}:refusal`;
|
|
166
338
|
out.push({ type: "text-start", id }, { type: "text-delta", id, delta: event.answer }, { type: "text-end", id });
|
|
167
339
|
}
|
|
@@ -228,7 +400,7 @@ var FoirRunChunks = class {
|
|
|
228
400
|
this.answering.add(node.id);
|
|
229
401
|
this.toRun.push(node);
|
|
230
402
|
} else {
|
|
231
|
-
this.pageCalls.
|
|
403
|
+
this.pageCalls.set(node.id, node.reply ?? null);
|
|
232
404
|
}
|
|
233
405
|
return out;
|
|
234
406
|
}
|
|
@@ -348,7 +520,6 @@ function agentsOf(client, caller) {
|
|
|
348
520
|
startAgentRun: refuse,
|
|
349
521
|
onRun: refuse,
|
|
350
522
|
decideAgentApproval: refuse,
|
|
351
|
-
answerPageTool: refuse,
|
|
352
523
|
agentThread: refuse,
|
|
353
524
|
createAgentAttachment: refuse
|
|
354
525
|
};
|
|
@@ -394,6 +565,10 @@ function foirChatTransport(options) {
|
|
|
394
565
|
}
|
|
395
566
|
return c;
|
|
396
567
|
};
|
|
568
|
+
const answerPage = async (id, reply, output) => {
|
|
569
|
+
if (!reply) throw new Error(`The call ${id} came without its reply, so the page cannot answer it.`);
|
|
570
|
+
await replyToStep(reply, { result: output ?? null });
|
|
571
|
+
};
|
|
397
572
|
const answerCall = async (node, signal) => {
|
|
398
573
|
const tool = pageTools[node.key];
|
|
399
574
|
let output;
|
|
@@ -404,7 +579,7 @@ function foirChatTransport(options) {
|
|
|
404
579
|
} catch (err) {
|
|
405
580
|
output = { error: err instanceof Error ? err.message : String(err) };
|
|
406
581
|
}
|
|
407
|
-
await
|
|
582
|
+
await answerPage(node.id, node.reply, output);
|
|
408
583
|
};
|
|
409
584
|
const follow = (runId, fresh, signal) => {
|
|
410
585
|
const chunks = chunksOf(runId);
|
|
@@ -456,7 +631,7 @@ function foirChatTransport(options) {
|
|
|
456
631
|
async sendMessages({ chatId, messages, abortSignal }) {
|
|
457
632
|
const decisions = decisionsIn(messages, decided);
|
|
458
633
|
const waiting = /* @__PURE__ */ new Set();
|
|
459
|
-
for (const c of runs.values()) for (const id2 of c.pageCalls) waiting.add(id2);
|
|
634
|
+
for (const c of runs.values()) for (const id2 of c.pageCalls.keys()) waiting.add(id2);
|
|
460
635
|
const answers = pageAnswersIn(messages, waiting);
|
|
461
636
|
if (decisions.length > 0 || answers.length > 0) {
|
|
462
637
|
for (const d of decisions) {
|
|
@@ -464,7 +639,9 @@ function foirChatTransport(options) {
|
|
|
464
639
|
decided.add(d.id);
|
|
465
640
|
}
|
|
466
641
|
for (const a of answers) {
|
|
467
|
-
|
|
642
|
+
let reply;
|
|
643
|
+
for (const c of runs.values()) reply ??= c.pageCalls.get(a.id);
|
|
644
|
+
await answerPage(a.id, reply, a.output);
|
|
468
645
|
for (const c of runs.values()) c.pageCalls.delete(a.id);
|
|
469
646
|
}
|
|
470
647
|
let runId = lastRun.get(chatId);
|
package/dist/app/client.d.ts
CHANGED
|
@@ -462,11 +462,15 @@ export interface AppClient {
|
|
|
462
462
|
deleted: boolean;
|
|
463
463
|
}>;
|
|
464
464
|
/**
|
|
465
|
-
* Drop what the data API has cached for one record, by
|
|
466
|
-
* so its next read resolves it again. False when Foir
|
|
467
|
-
* record. For a source read live from its host.
|
|
465
|
+
* Drop what the data API has cached for one record, named by its id or
|
|
466
|
+
* by its natural key, so its next read resolves it again. False when Foir
|
|
467
|
+
* holds no such record. For a source read live from its host.
|
|
468
468
|
*/
|
|
469
|
-
refresh(model: string,
|
|
469
|
+
refresh(model: string, record: {
|
|
470
|
+
id: string;
|
|
471
|
+
} | {
|
|
472
|
+
naturalKey: string;
|
|
473
|
+
}): Promise<{
|
|
470
474
|
refreshed: boolean;
|
|
471
475
|
}>;
|
|
472
476
|
/**
|
|
@@ -766,7 +770,7 @@ export interface AppStartedOperation {
|
|
|
766
770
|
/** True when `concurrencyKey` matched a run in flight, so nothing was started. */
|
|
767
771
|
joined: boolean;
|
|
768
772
|
/** True when `idempotencyKey` matched a run in flight or completed, so nothing was started. */
|
|
769
|
-
|
|
773
|
+
replayed: boolean;
|
|
770
774
|
}
|
|
771
775
|
/** Build the client an installed app calls Foir with. See the module doc. */
|
|
772
776
|
export declare function createAppClient(config: AppClientConfig): AppClient;
|
package/dist/app.js
CHANGED
|
@@ -632,10 +632,16 @@ function createAppClient(config) {
|
|
|
632
632
|
const deleted = await field(name, `mutation AppDeleteRecord($id: String!) { ${name}(id: $id) }`, { id });
|
|
633
633
|
return { deleted: deleted === true };
|
|
634
634
|
},
|
|
635
|
-
async refresh(model,
|
|
636
|
-
|
|
635
|
+
async refresh(model, record) {
|
|
636
|
+
const byId = "id" in record;
|
|
637
|
+
const value = byId ? record.id : record.naturalKey;
|
|
638
|
+
if (value === "") refuse(`a record refresh on ${model} was given an empty name`);
|
|
637
639
|
const name = `refresh${appModelTypeName(model)}`;
|
|
638
|
-
const refreshed = await field(
|
|
640
|
+
const refreshed = await field(
|
|
641
|
+
name,
|
|
642
|
+
byId ? `mutation AppRefreshRecord($id: ID!) { ${name}(id: $id) }` : `mutation AppRefreshRecord($naturalKey: String!) { ${name}(naturalKey: $naturalKey) }`,
|
|
643
|
+
byId ? { id: value } : { naturalKey: value }
|
|
644
|
+
);
|
|
639
645
|
if (typeof refreshed !== "boolean") return refuse(`${name} reported no error and did not say whether it refreshed the record`);
|
|
640
646
|
return { refreshed };
|
|
641
647
|
},
|
|
@@ -972,12 +978,12 @@ function createAppClient(config) {
|
|
|
972
978
|
args.push("concurrencyKey: $concurrencyKey");
|
|
973
979
|
variables.concurrencyKey = options.concurrencyKey;
|
|
974
980
|
}
|
|
975
|
-
const document = `mutation AppStartOperation(${params.join(", ")}) { startOperation(${args.join(", ")}) { executionId joined
|
|
981
|
+
const document = `mutation AppStartOperation(${params.join(", ")}) { startOperation(${args.join(", ")}) { executionId joined replayed } }`;
|
|
976
982
|
const keyed = Boolean(options.idempotencyKey || options.concurrencyKey);
|
|
977
983
|
const data = keyed ? await run(document, variables) : await send(document, variables);
|
|
978
984
|
const answer = data["startOperation"];
|
|
979
985
|
if (!answer?.executionId) return refuse("startOperation reported no error and named no run");
|
|
980
|
-
return { executionId: answer.executionId, joined: answer.joined === true,
|
|
986
|
+
return { executionId: answer.executionId, joined: answer.joined === true, replayed: answer.replayed === true };
|
|
981
987
|
}
|
|
982
988
|
}
|
|
983
989
|
};
|
|
@@ -1067,18 +1073,11 @@ function readCallback(value) {
|
|
|
1067
1073
|
if (!isObject(value)) return void 0;
|
|
1068
1074
|
const token = text(value.token);
|
|
1069
1075
|
if (!token) return void 0;
|
|
1070
|
-
const mutations = isObject(value.mutations) ? value.mutations : {};
|
|
1071
1076
|
const signingKey = text(value.signingKey);
|
|
1072
1077
|
return {
|
|
1073
|
-
|
|
1078
|
+
endpoint: text(value.endpoint) ?? "",
|
|
1074
1079
|
token,
|
|
1075
1080
|
expiresAt: text(value.expiresAt) ?? "",
|
|
1076
|
-
mutations: {
|
|
1077
|
-
complete: text(mutations.complete) ?? "",
|
|
1078
|
-
fail: text(mutations.fail) ?? "",
|
|
1079
|
-
progress: text(mutations.progress) ?? "",
|
|
1080
|
-
cancel: text(mutations.cancel) ?? ""
|
|
1081
|
-
},
|
|
1082
1081
|
...Array.isArray(value.causationChain) ? { causationChain: value.causationChain.map(String) } : {},
|
|
1083
1082
|
...signingKey ? { signingKey } : {}
|
|
1084
1083
|
};
|