js-bao-wss-client 2.2.0-alpha.11 → 2.2.0-alpha.12
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/README.md +128 -797
- package/dist/JsBaoClient.d.ts +166 -15
- package/dist/JsBaoClient.js +319 -126
- package/dist/api/cronTriggersApi.d.ts +26 -0
- package/dist/api/cronTriggersApi.js +26 -0
- package/dist/api/databasesApi.d.ts +84 -21
- package/dist/api/databasesApi.js +76 -14
- package/dist/api/documentsApi.d.ts +194 -0
- package/dist/api/documentsApi.js +30 -0
- package/dist/api/functionsApi.d.ts +42 -53
- package/dist/api/functionsApi.js +98 -47
- package/dist/api/geminiApi.d.ts +18 -18
- package/dist/api/geminiApi.js +10 -10
- package/dist/api/llmApi.d.ts +10 -10
- package/dist/api/llmApi.js +6 -6
- package/dist/browser.umd.js +2716 -2097
- package/dist/errors.d.ts +10 -3
- package/dist/errors.js +10 -3
- package/dist/internal/blobManager.js +7 -6
- package/dist/internal/documentManager.d.ts +27 -0
- package/dist/internal/documentManager.js +56 -1
- package/dist/internal/functionRunStatus.d.ts +39 -0
- package/dist/internal/functionRunStatus.js +45 -0
- package/dist/internal/httpClient.d.ts +28 -0
- package/dist/internal/httpClient.js +60 -16
- package/dist/internal/large-documents/document-format.d.ts +48 -0
- package/dist/internal/large-documents/document-format.js +82 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,9 +17,8 @@ A TypeScript/JavaScript client library for js-bao-wss that provides HTTP APIs an
|
|
|
17
17
|
- **Token Management**: Proactive refresh in HTTP calls
|
|
18
18
|
- **Analytics**: Buffered event logging API with optional automatic lifecycle events
|
|
19
19
|
- **Blob Storage**: Upload/list/get/downloadUrl/delete per document with offline cache
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **Workflow Apply**: Single-client claim/confirm flow for applying workflow results to Yjs documents
|
|
20
|
+
- **Server Functions**: Invoke your app's server functions, start and wait on long-running runs, and join the channels they authorize
|
|
21
|
+
- **LLM**: Chat API and model listing _(deprecated — run a managed prompt from a server function (`ctx.prompts.run`) instead)_
|
|
23
22
|
- **Offline-first Open**: Non-blocking open with IndexedDB-backed cache
|
|
24
23
|
- **Offline Blob Cache**: Cache API + IndexedDB backed uploads/reads with eviction and retry
|
|
25
24
|
- **Network Controls**: Online/offline modes, reachability, connection control
|
|
@@ -1160,6 +1159,26 @@ const { metadata } = await client.documents.create({
|
|
|
1160
1159
|
});
|
|
1161
1160
|
console.log("Created document:", metadata.documentId);
|
|
1162
1161
|
|
|
1162
|
+
// Create a LARGE document (`documentFormat: 2`) — records live in a persisted
|
|
1163
|
+
// local store and in the server's own table, so it scales far past an ordinary
|
|
1164
|
+
// document's ~10 MB. The choice is made here and is never migrated. The
|
|
1165
|
+
// returned metadata reports the format immediately, before the server commit
|
|
1166
|
+
// lands, and `documents.get(id).documentFormat` reports it afterwards.
|
|
1167
|
+
const large = await client.documents.create({
|
|
1168
|
+
title: "Ledger",
|
|
1169
|
+
documentFormat: 2,
|
|
1170
|
+
});
|
|
1171
|
+
console.log("Format:", large.metadata.documentFormat); // 2
|
|
1172
|
+
|
|
1173
|
+
// Omitting `documentFormat` (or passing `1`) creates an ordinary document and
|
|
1174
|
+
// sends nothing extra. Any other value is refused before anything is created,
|
|
1175
|
+
// with `JsBaoError` code `INVALID_ARGUMENT`; `localOnly: true` cannot be
|
|
1176
|
+
// combined with `documentFormat: 2` (`LOCAL_ONLY_UNSUPPORTED_OPTION`), because
|
|
1177
|
+
// a large document's records live in a store only the server's room opens.
|
|
1178
|
+
// `documents.createWithAlias` takes the same option with the same effect.
|
|
1179
|
+
// A large document needs a local store that outlives the session: Node has
|
|
1180
|
+
// one, and a browser needs `databaseConfig: { type: "opfs", ... }`.
|
|
1181
|
+
|
|
1163
1182
|
// List documents. There are two readers, and they are disjoint: the documents
|
|
1164
1183
|
// the user owns, and the documents shared with them. Reading both is what it
|
|
1165
1184
|
// takes to see everything the user has access to.
|
|
@@ -1232,6 +1251,16 @@ console.log(result.documentId); // The created document ID
|
|
|
1232
1251
|
console.log(result.alias.aliasKey); // "current-draft"
|
|
1233
1252
|
console.log(result.alias.documentId); // Same as documentId
|
|
1234
1253
|
|
|
1254
|
+
// The alias create takes `documentFormat` too, so a large document can be
|
|
1255
|
+
// created with its alias atomically. The response carries `documentFormat: 2`
|
|
1256
|
+
// for a large document and omits the key for an ordinary one.
|
|
1257
|
+
const ledger = await client.documents.createWithAlias({
|
|
1258
|
+
title: "Ledger",
|
|
1259
|
+
alias: { scope: "user", aliasKey: "ledger" },
|
|
1260
|
+
documentFormat: 2,
|
|
1261
|
+
});
|
|
1262
|
+
console.log(ledger.documentFormat); // 2
|
|
1263
|
+
|
|
1235
1264
|
// Attempting to create with an existing alias throws HTTP 409
|
|
1236
1265
|
try {
|
|
1237
1266
|
await client.documents.createWithAlias({
|
|
@@ -1721,7 +1750,7 @@ console.log(user.name, user.email, user.appRole);
|
|
|
1721
1750
|
|
|
1722
1751
|
## Large Language Models (LLM)
|
|
1723
1752
|
|
|
1724
|
-
> **Deprecated.** The direct LLM client API (`client.llm.*`) is deprecated and will be removed in a future major release.
|
|
1753
|
+
> **Deprecated.** The direct LLM client API (`client.llm.*`) is deprecated and will be removed in a future major release. Run a managed prompt from a server function (`ctx.prompts.run`) instead.
|
|
1725
1754
|
|
|
1726
1755
|
```typescript
|
|
1727
1756
|
// List available models
|
|
@@ -1767,7 +1796,7 @@ console.log(imageReply.content);
|
|
|
1767
1796
|
|
|
1768
1797
|
## Gemini
|
|
1769
1798
|
|
|
1770
|
-
> **Deprecated.** The direct Gemini client API (`client.gemini.*`) is deprecated and will be removed in a future major release.
|
|
1799
|
+
> **Deprecated.** The direct Gemini client API (`client.gemini.*`) is deprecated and will be removed in a future major release. Run a managed prompt from a server function (`ctx.prompts.run`) instead.
|
|
1771
1800
|
|
|
1772
1801
|
```typescript
|
|
1773
1802
|
// Text generation with optional structured output
|
|
@@ -1851,756 +1880,142 @@ console.log(tokens.totalTokens);
|
|
|
1851
1880
|
- `POST /gemini/count-tokens` → `client.gemini.countTokens(...)`
|
|
1852
1881
|
- `POST /gemini/generate-raw` → `client.gemini.generateRaw({ model, body })`
|
|
1853
1882
|
These endpoints run server-side, so browser clients never see Google credentials.
|
|
1854
|
-
-
|
|
1855
|
-
- `GEMINI_DEFAULT_MODEL` (e.g. `models/gemini-2.5-flash`)
|
|
1856
|
-
- `GEMINI_ALLOWED_MODELS` (comma-separated allowlist such as `models/gemini-2.5-flash,models/gemini-2.5-pro`).
|
|
1857
|
-
- Inline multimodal content uses base64 `parts` (text, image, file). Large file uploads can be added later via the Gemini Files API; V1 augments prompts with base64-inlined payloads up to ~25 MB.
|
|
1883
|
+
- Inline multimodal content uses base64 `parts` (text, image, file), up to about 25 MB per request.
|
|
1858
1884
|
- Structured output leverages `generationConfig.responseMimeType` plus optional `responseSchema` / `responseJsonSchema` so Gemini can return deterministic JSON. The full server response is always available via the `raw` field when you need annotations or safety metadata.
|
|
1859
1885
|
- Error handling surfaces `JsBaoError` with `code: "GEMINI_ERROR"`; the `details` property contains the raw upstream payload so you can log or render troubleshooting info.
|
|
1860
|
-
- See `.dev.local.example` for sample environment values and `docs/gemini-direct-plan.md` for architectural details.
|
|
1861
|
-
|
|
1862
|
-
## Workflows
|
|
1863
|
-
|
|
1864
|
-
Workflows allow you to execute server-side, multi-step processes that can include LLM calls, delays, transformations, and more. The client provides APIs to start workflows, monitor their status, and receive real-time completion events.
|
|
1865
|
-
|
|
1866
|
-
### Starting a Workflow
|
|
1867
|
-
|
|
1868
|
-
```typescript
|
|
1869
|
-
// Start a workflow with input data
|
|
1870
|
-
const result = await client.workflows.start({
|
|
1871
|
-
workflowKey: "my-workflow-key",
|
|
1872
|
-
input: {
|
|
1873
|
-
message: "Hello world",
|
|
1874
|
-
value: 42,
|
|
1875
|
-
},
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
console.log("Run started:", result.runKey);
|
|
1879
|
-
console.log("Run ID:", result.runId);
|
|
1880
|
-
console.log("Status:", result.status);
|
|
1881
|
-
```
|
|
1882
|
-
|
|
1883
|
-
#### Start Options
|
|
1884
|
-
|
|
1885
|
-
```typescript
|
|
1886
|
-
const result = await client.workflows.start({
|
|
1887
|
-
// The workflow key (required)
|
|
1888
|
-
workflowKey: "my-workflow-key",
|
|
1889
|
-
// Input data passed to the workflow (optional, defaults to {})
|
|
1890
|
-
input: { message: "Hello" },
|
|
1891
|
-
// Provide a custom runKey for idempotency (auto-generated if omitted)
|
|
1892
|
-
runKey: "unique-run-identifier",
|
|
1893
|
-
// Associate the run with a document
|
|
1894
|
-
contextDocId: "doc-123",
|
|
1895
|
-
// Pass additional metadata (max 1KB, returned in listRuns/getStatus)
|
|
1896
|
-
meta: { source: "user-action", priority: "high" },
|
|
1897
|
-
});
|
|
1898
|
-
```
|
|
1899
|
-
|
|
1900
|
-
### Document-Scoped Workflows
|
|
1901
|
-
|
|
1902
|
-
Workflows can be scoped to a specific document using `contextDocId`. This is useful when:
|
|
1903
|
-
- Processing document-specific data
|
|
1904
|
-
- Running workflows in the context of a particular document
|
|
1905
|
-
- Allowing the same `runKey` to be used across different documents
|
|
1906
|
-
|
|
1907
|
-
The workflow instance ID is built from `{appId}:{contextDocId}:{runKey}`, so a workflow started with one `contextDocId` cannot be accessed with a different one.
|
|
1908
|
-
|
|
1909
|
-
```typescript
|
|
1910
|
-
// Start a workflow scoped to a document
|
|
1911
|
-
const result = await client.workflows.start({
|
|
1912
|
-
workflowKey: "process-document",
|
|
1913
|
-
input: { action: "analyze" },
|
|
1914
|
-
contextDocId: "doc-123",
|
|
1915
|
-
runKey: "analyze-v1",
|
|
1916
|
-
});
|
|
1917
|
-
|
|
1918
|
-
// Must use the same contextDocId for all operations
|
|
1919
|
-
const status = await client.workflows.getStatus({
|
|
1920
|
-
workflowKey: "process-document",
|
|
1921
|
-
runKey: "analyze-v1",
|
|
1922
|
-
contextDocId: "doc-123",
|
|
1923
|
-
});
|
|
1924
|
-
await client.workflows.terminate({
|
|
1925
|
-
workflowKey: "process-document",
|
|
1926
|
-
runKey: "analyze-v1",
|
|
1927
|
-
contextDocId: "doc-123",
|
|
1928
|
-
});
|
|
1929
|
-
|
|
1930
|
-
// List runs for this document
|
|
1931
|
-
const runs = await client.workflows.listRuns({ contextDocId: "doc-123" });
|
|
1932
|
-
```
|
|
1933
|
-
|
|
1934
|
-
If `contextDocId` is omitted, the user's root document is used by default.
|
|
1935
|
-
|
|
1936
|
-
### Duplicate Workflow Protection (Idempotency)
|
|
1937
|
-
|
|
1938
|
-
When you provide a `runKey`, the server ensures only one workflow run exists for that key. If you call `start()` again with the same `runKey`, the existing run is returned instead of creating a new one:
|
|
1939
|
-
|
|
1940
|
-
```typescript
|
|
1941
|
-
// First call creates the workflow
|
|
1942
|
-
const first = await client.workflows.start({
|
|
1943
|
-
workflowKey: "process-document",
|
|
1944
|
-
input: { docId: "abc" },
|
|
1945
|
-
runKey: "process-abc-v1",
|
|
1946
|
-
});
|
|
1947
|
-
console.log(first.existing); // false - new run created
|
|
1948
|
-
|
|
1949
|
-
// Second call with same runKey returns existing run
|
|
1950
|
-
const second = await client.workflows.start({
|
|
1951
|
-
workflowKey: "process-document",
|
|
1952
|
-
input: { docId: "abc" },
|
|
1953
|
-
runKey: "process-abc-v1",
|
|
1954
|
-
});
|
|
1955
|
-
console.log(second.existing); // true - existing run returned
|
|
1956
|
-
console.log(second.runId === first.runId); // true - same run
|
|
1957
|
-
```
|
|
1958
|
-
|
|
1959
|
-
This is useful for:
|
|
1960
|
-
- Preventing duplicate processing when users click a button multiple times
|
|
1961
|
-
- Safely retrying failed requests without creating duplicate work
|
|
1962
|
-
- Implementing exactly-once semantics for critical operations
|
|
1963
|
-
|
|
1964
|
-
### Checking Workflow Status
|
|
1965
|
-
|
|
1966
|
-
Poll the status of a running workflow:
|
|
1967
|
-
|
|
1968
|
-
```typescript
|
|
1969
|
-
const status = await client.workflows.getStatus({
|
|
1970
|
-
workflowKey: "my-workflow-key",
|
|
1971
|
-
runKey,
|
|
1972
|
-
});
|
|
1973
|
-
|
|
1974
|
-
// One canonical value: "queued" | "running" | "apply_pending" | "apply_claimed"
|
|
1975
|
-
// | "completed" | "failed" | "terminated" | "missing" | "skipped"
|
|
1976
|
-
console.log("Status:", status.status);
|
|
1977
1886
|
|
|
1978
|
-
|
|
1979
|
-
console.log("Output:", status.output);
|
|
1980
|
-
}
|
|
1887
|
+
## Server Functions
|
|
1981
1888
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1889
|
+
A server function is TypeScript your app's team writes in its config tree and
|
|
1890
|
+
pushes with `primitive config push`. The client calls it by key, and the
|
|
1891
|
+
function's `access` gate decides who may. Inside, the function works on the
|
|
1892
|
+
app's authority: it reads and writes databases and documents, runs saved
|
|
1893
|
+
prompts, calls integrations, and publishes to channels. The Server Functions
|
|
1894
|
+
guide covers writing one; this section covers calling one.
|
|
1990
1895
|
|
|
1991
|
-
|
|
1992
|
-
// The run did not run and did not fail: its declarative lock was held and the
|
|
1993
|
-
// workflow declared `onContention = "ignore"`. `skipReason` says why.
|
|
1994
|
-
console.log("Did not run:", status.skipReason); // "LOCK_CONTENTION"
|
|
1995
|
-
}
|
|
1996
|
-
```
|
|
1896
|
+
### Invoking a Function
|
|
1997
1897
|
|
|
1998
|
-
|
|
1898
|
+
`client.functions.invoke` runs the function inside the request and resolves
|
|
1899
|
+
with its result:
|
|
1999
1900
|
|
|
2000
1901
|
```typescript
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
input: { data: "..." },
|
|
2005
|
-
contextDocId: "doc-123",
|
|
1902
|
+
const result = await client.functions.invoke<{ message: string }>("greet", {
|
|
1903
|
+
input: { name: "Ada" }, // what the handler receives as `input`
|
|
1904
|
+
timeoutMs: 10_000, // default 5 000, clamped to 30 000
|
|
2006
1905
|
});
|
|
2007
1906
|
|
|
2008
|
-
// Get status - must provide the same contextDocId
|
|
2009
|
-
const status = await client.workflows.getStatus({
|
|
2010
|
-
workflowKey: "my-workflow",
|
|
2011
|
-
runKey: result.runKey,
|
|
2012
|
-
contextDocId: "doc-123",
|
|
2013
|
-
});
|
|
2014
|
-
```
|
|
2015
|
-
|
|
2016
|
-
If `contextDocId` is omitted, the user's root document is used by default.
|
|
2017
|
-
|
|
2018
|
-
### Waiting for Workflow Completion
|
|
2019
|
-
|
|
2020
|
-
For an async run, `waitFor(runId)` resolves once the run reaches a terminal
|
|
2021
|
-
state — no manual subscribe/poll/timeout loop:
|
|
2022
|
-
|
|
2023
|
-
```typescript
|
|
2024
|
-
const { runId } = await client.workflows.start({
|
|
2025
|
-
workflowKey: "process-document",
|
|
2026
|
-
input: { action: "analyze" },
|
|
2027
|
-
});
|
|
2028
|
-
|
|
2029
|
-
const result = await client.workflows.waitFor(runId);
|
|
2030
|
-
// result: { status, output?, error? }
|
|
2031
|
-
|
|
2032
1907
|
if (result.status === "completed") {
|
|
2033
|
-
console.log(
|
|
2034
|
-
} else
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
```
|
|
2038
|
-
|
|
2039
|
-
`waitFor` is event-driven: it subscribes to the same `workflowStatus`
|
|
2040
|
-
WebSocket frame (no polling) and does a single reconcile fetch right after
|
|
2041
|
-
subscribing to catch a run that already finished before you called it. On a
|
|
2042
|
-
WebSocket reconnect it re-runs that single reconcile — there is no interval
|
|
2043
|
-
timer.
|
|
2044
|
-
|
|
2045
|
-
- **Resolves on every terminal state**: `completed`, `failed`, `terminated`,
|
|
2046
|
-
`skipped`, and the apply-ready states `apply_pending` / `apply_claimed` (the
|
|
2047
|
-
run finished and its result is available for the client-side apply step). The
|
|
2048
|
-
server reconciles a run's status before reporting it, so the reported
|
|
2049
|
-
terminal state is the run's real one — a terminated run resolves as
|
|
2050
|
-
`"terminated"`, never as `"completed"`.
|
|
2051
|
-
- **`skipped` needs this client at 2.1.0 or newer.** It is the outcome of a run
|
|
2052
|
-
whose declarative lock was held under `onContention = "ignore"`: the run did
|
|
2053
|
-
not run and did not fail, and `result.skipReason` says why
|
|
2054
|
-
(`"LOCK_CONTENTION"`). An older client does not recognise it as terminal, so
|
|
2055
|
-
a wait on an elided run keeps waiting until its own timeout instead of
|
|
2056
|
-
returning — upgrade before you switch a workflow to `ignore`.
|
|
2057
|
-
- **Rejects only** on two conditions: the run is unknown or not readable by the
|
|
2058
|
-
caller (a `NOT_FOUND` `JsBaoError`, surfaced immediately — it does not wait
|
|
2059
|
-
for the timeout), or the timeout elapses (a `WORKFLOW_WAIT_TIMEOUT`
|
|
2060
|
-
`JsBaoError`).
|
|
2061
|
-
|
|
2062
|
-
```typescript
|
|
2063
|
-
import { isJsBaoError } from "js-bao-wss-client";
|
|
2064
|
-
|
|
2065
|
-
try {
|
|
2066
|
-
const result = await client.workflows.waitFor(runId, { timeoutMs: 60000 });
|
|
2067
|
-
// ...
|
|
2068
|
-
} catch (err) {
|
|
2069
|
-
if (isJsBaoError(err) && err.code === "WORKFLOW_WAIT_TIMEOUT") {
|
|
2070
|
-
// still running after the deadline
|
|
2071
|
-
} else if (isJsBaoError(err) && err.code === "NOT_FOUND") {
|
|
2072
|
-
// unknown or not-owned runId
|
|
2073
|
-
}
|
|
1908
|
+
console.log(result.output?.message); // "hello Ada"
|
|
1909
|
+
} else {
|
|
1910
|
+
// "failed" — the handler threw or its output failed outputSchema — or "timeout"
|
|
1911
|
+
console.error(result.status, result.error, result.errorCode);
|
|
2074
1912
|
}
|
|
2075
1913
|
```
|
|
2076
1914
|
|
|
2077
|
-
|
|
2078
|
-
the
|
|
2079
|
-
|
|
1915
|
+
- `status` is `completed`, `failed`, or `timeout`. All three resolve: the call
|
|
1916
|
+
reached the function and the function has an answer. The promise rejects only
|
|
1917
|
+
when the platform refused before the code ran — the `access` gate or a
|
|
1918
|
+
disabled function (`403`), an unknown key (`404`), a function with no pushed
|
|
1919
|
+
code (`409`), or the rate ceiling (`429`, or `503` when the ceiling cannot be
|
|
1920
|
+
checked).
|
|
1921
|
+
- Options are all optional: `input`, `contextDocId` (a document the call is
|
|
1922
|
+
about), `meta` (caller metadata, at most 1 KB encoded), and `timeoutMs`.
|
|
1923
|
+
- `invocationId` names the invocation-log record for the call. Pass it to
|
|
1924
|
+
`primitive functions logs <functionId> --invocation <id>` to read what the
|
|
1925
|
+
function printed and the error it threw.
|
|
2080
1926
|
|
|
2081
|
-
|
|
2082
|
-
WebSocket connection (see [Listening for Workflow
|
|
2083
|
-
Events](#listening-for-workflow-events) below); without one, `waitFor` still
|
|
2084
|
-
resolves via its reconcile fetch, just less immediately.
|
|
1927
|
+
### Starting a Long-Running Run
|
|
2085
1928
|
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
1929
|
+
`client.functions.start` runs the same function as a task: it answers with a
|
|
1930
|
+
run id immediately and the code keeps running. Every pushed function accepts
|
|
1931
|
+
both `invoke` and `start`; the caller chooses.
|
|
2089
1932
|
|
|
2090
1933
|
```typescript
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
console.log("Instance:", event.instanceId);
|
|
2095
|
-
console.log("Meta:", event.meta);
|
|
1934
|
+
const run = await client.functions.start("order-sync", {
|
|
1935
|
+
input: { orderId },
|
|
1936
|
+
runKey: `order-${orderId}`, // a repeat replays the existing run (existing: true)
|
|
2096
1937
|
});
|
|
2097
1938
|
|
|
2098
|
-
//
|
|
2099
|
-
client.
|
|
2100
|
-
|
|
2101
|
-
// "completed" | "failed" | "terminated" | "skipped"
|
|
2102
|
-
console.log("Status:", event.status);
|
|
1939
|
+
// Poll once…
|
|
1940
|
+
const now = await client.functions.getStatus(run.runId);
|
|
1941
|
+
console.log(now.status);
|
|
2103
1942
|
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
if (event.status === "failed") {
|
|
2109
|
-
console.log("Error:", event.error);
|
|
2110
|
-
// Classified failures also carry a code, so you branch on a constant
|
|
2111
|
-
// instead of matching `error` text: "LOCK_CONTENTION" | "LOCK_TIMEOUT".
|
|
2112
|
-
if (event.errorCode === "LOCK_CONTENTION") {
|
|
2113
|
-
console.log("An import is already in progress");
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
if (event.status === "skipped") {
|
|
2118
|
-
// The run did not run and did not fail: its declarative lock was held and
|
|
2119
|
-
// the workflow declared `onContention = "ignore"`. No `error` is sent.
|
|
2120
|
-
console.log("Did not run:", event.skipReason); // "LOCK_CONTENTION"
|
|
2121
|
-
}
|
|
1943
|
+
// …or wait for a terminal state.
|
|
1944
|
+
const settled = await client.functions.waitFor<{ confirmedAt: number }>(run.runId, {
|
|
1945
|
+
timeoutMs: 60_000, // default 15 minutes; throws WORKFLOW_WAIT_TIMEOUT past it
|
|
2122
1946
|
});
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
```typescript
|
|
2128
|
-
// Open a document to establish WebSocket for receiving notifications
|
|
2129
|
-
await client.documents.open(documentId);
|
|
2130
|
-
|
|
2131
|
-
// Now workflow events will be delivered
|
|
2132
|
-
const result = await client.workflows.start({ workflowKey: "my-workflow", input: { data: "..." } });
|
|
2133
|
-
```
|
|
2134
|
-
|
|
2135
|
-
#### Event Payload
|
|
2136
|
-
|
|
2137
|
-
```typescript
|
|
2138
|
-
interface WorkflowStatusEvent {
|
|
2139
|
-
type: "workflowStatus";
|
|
2140
|
-
workflowKey: string;
|
|
2141
|
-
workflowId: string;
|
|
2142
|
-
runKey: string;
|
|
2143
|
-
runId: string;
|
|
2144
|
-
// "skipped" needs this client at 2.1.0 or newer (see Waiting for Workflow
|
|
2145
|
-
// Completion). It is the outcome of a run whose declarative lock was held
|
|
2146
|
-
// and whose workflow declared `onContention = "ignore"`.
|
|
2147
|
-
status: "completed" | "failed" | "terminated" | "skipped";
|
|
2148
|
-
output?: any;
|
|
2149
|
-
error?: string;
|
|
2150
|
-
// Platform classification of a FAILED run. Absent on every other status, and
|
|
2151
|
-
// absent on a failure the platform did not classify.
|
|
2152
|
-
errorCode?: "LOCK_CONTENTION" | "LOCK_TIMEOUT";
|
|
2153
|
-
// Why a "skipped" run did not run. Absent on every other status; a skipped
|
|
2154
|
-
// frame carries no `error`.
|
|
2155
|
-
skipReason?: "LOCK_CONTENTION";
|
|
2156
|
-
contextDocId?: string;
|
|
2157
|
-
needsApply?: boolean; // true when workflow requires client-side apply
|
|
2158
|
-
startedByUserId?: string;
|
|
2159
|
-
meta?: Record<string, any>; // custom metadata passed to workflows.start()
|
|
2160
|
-
}
|
|
2161
|
-
```
|
|
2162
|
-
|
|
2163
|
-
### Listing Workflow Runs
|
|
2164
|
-
|
|
2165
|
-
View all workflow runs for the current user:
|
|
2166
|
-
|
|
2167
|
-
```typescript
|
|
2168
|
-
// List all runs
|
|
2169
|
-
const runs = await client.workflows.listRuns();
|
|
2170
|
-
console.log("Total runs:", runs.items.length);
|
|
2171
|
-
|
|
2172
|
-
runs.items.forEach((run) => {
|
|
2173
|
-
console.log(run.runKey, run.status, run.startedAt);
|
|
2174
|
-
});
|
|
2175
|
-
|
|
2176
|
-
// Filter by workflow
|
|
2177
|
-
const filtered = await client.workflows.listRuns({
|
|
2178
|
-
workflowKey: "my-workflow",
|
|
2179
|
-
});
|
|
2180
|
-
|
|
2181
|
-
// Filter by status
|
|
2182
|
-
const running = await client.workflows.listRuns({
|
|
2183
|
-
status: "running",
|
|
2184
|
-
});
|
|
2185
|
-
|
|
2186
|
-
// Filter by document - list runs associated with a specific document
|
|
2187
|
-
const docRuns = await client.workflows.listRuns({
|
|
2188
|
-
contextDocId: "doc-123",
|
|
2189
|
-
});
|
|
2190
|
-
|
|
2191
|
-
// Pagination
|
|
2192
|
-
const page1 = await client.workflows.listRuns({ limit: 10 });
|
|
2193
|
-
if (page1.cursor) {
|
|
2194
|
-
const page2 = await client.workflows.listRuns({
|
|
2195
|
-
limit: 10,
|
|
2196
|
-
cursor: page1.cursor,
|
|
2197
|
-
});
|
|
1947
|
+
if (settled.status === "failed") {
|
|
1948
|
+
console.error("order-sync failed:", settled.error?.message);
|
|
1949
|
+
} else {
|
|
1950
|
+
console.log(settled.status, settled.output?.confirmedAt);
|
|
2198
1951
|
}
|
|
2199
1952
|
|
|
2200
|
-
//
|
|
2201
|
-
|
|
2202
|
-
const oldestFirst = await client.workflows.listRuns({ forward: true });
|
|
2203
|
-
|
|
2204
|
-
// Combine filters
|
|
2205
|
-
const combined = await client.workflows.listRuns({
|
|
2206
|
-
contextDocId: "doc-123",
|
|
2207
|
-
status: "completed",
|
|
2208
|
-
forward: true,
|
|
2209
|
-
limit: 20,
|
|
2210
|
-
});
|
|
2211
|
-
```
|
|
2212
|
-
|
|
2213
|
-
#### Run Record Fields
|
|
2214
|
-
|
|
2215
|
-
```typescript
|
|
2216
|
-
type WorkflowRunStatus =
|
|
2217
|
-
| "queued"
|
|
2218
|
-
| "running"
|
|
2219
|
-
| "apply_pending"
|
|
2220
|
-
| "apply_claimed"
|
|
2221
|
-
| "completed"
|
|
2222
|
-
| "failed"
|
|
2223
|
-
| "terminated"
|
|
2224
|
-
| "missing"
|
|
2225
|
-
| "skipped";
|
|
2226
|
-
|
|
2227
|
-
interface WorkflowRun {
|
|
2228
|
-
runId: string;
|
|
2229
|
-
runKey: string;
|
|
2230
|
-
instanceId: string;
|
|
2231
|
-
workflowId: string;
|
|
2232
|
-
workflowKey: string;
|
|
2233
|
-
revisionId: string;
|
|
2234
|
-
contextDocId?: string;
|
|
2235
|
-
status: WorkflowRunStatus;
|
|
2236
|
-
startedAt?: string; // ISO timestamp stamped when the run started
|
|
2237
|
-
endedAt?: string;
|
|
2238
|
-
errorMessage?: string | null; // set when status is "failed", null otherwise
|
|
2239
|
-
// The platform's own classification of a failure, when it has one:
|
|
2240
|
-
// "LOCK_CONTENTION" (a declarative lock was held under onContention = "fail")
|
|
2241
|
-
// or "LOCK_TIMEOUT" (an onContention = "block" budget ran out). null for every
|
|
2242
|
-
// unclassified failure. Branch on this rather than on errorMessage text.
|
|
2243
|
-
errorCode?: "LOCK_CONTENTION" | "LOCK_TIMEOUT" | null;
|
|
2244
|
-
// Why a "skipped" run did not run: "LOCK_CONTENTION". null for any other
|
|
2245
|
-
// status. Such a run carries no errorMessage and emits no error analytics.
|
|
2246
|
-
skipReason?: "LOCK_CONTENTION" | null;
|
|
2247
|
-
meta?: Record<string, any>; // custom metadata passed to workflows.start()
|
|
2248
|
-
}
|
|
1953
|
+
// Stop a run that is still going.
|
|
1954
|
+
await client.functions.terminate({ functionKey: "order-sync", runKey: run.runKey });
|
|
2249
1955
|
```
|
|
2250
1956
|
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
runKey,
|
|
2259
|
-
});
|
|
2260
|
-
console.log("Terminated, final status:", result.status);
|
|
2261
|
-
```
|
|
2262
|
-
|
|
2263
|
-
If you started the workflow with a specific `contextDocId`, you must provide the same `contextDocId` when terminating:
|
|
2264
|
-
|
|
2265
|
-
```typescript
|
|
2266
|
-
// Terminate a workflow started with contextDocId
|
|
2267
|
-
const result = await client.workflows.terminate({
|
|
2268
|
-
workflowKey: "my-workflow-key",
|
|
2269
|
-
runKey,
|
|
2270
|
-
contextDocId: "doc-123",
|
|
2271
|
-
});
|
|
2272
|
-
```
|
|
2273
|
-
|
|
2274
|
-
### Applying Workflow Results to Documents
|
|
2275
|
-
|
|
2276
|
-
When a workflow completes, its result can be applied to a Yjs document by exactly one connected client. This prevents duplicate writes when multiple tabs or devices are connected to the same document.
|
|
2277
|
-
|
|
2278
|
-
Use `workflows.define()` to register an apply handler at initialization. The client automatically claims, applies, and confirms workflow results:
|
|
2279
|
-
|
|
2280
|
-
```typescript
|
|
2281
|
-
// Define a workflow with its apply handler (call once at startup)
|
|
2282
|
-
client.workflows.define("my-workflow-key", {
|
|
2283
|
-
onApply: async ({ output, workflowKey, runKey, contextDocId, meta }) => {
|
|
2284
|
-
// Use models to apply the workflow result to the document
|
|
2285
|
-
const contact = new Contact({
|
|
2286
|
-
name: output.contactName,
|
|
2287
|
-
email: output.contactEmail,
|
|
2288
|
-
});
|
|
2289
|
-
await contact.save({ targetDocument: contextDocId });
|
|
2290
|
-
|
|
2291
|
-
// `meta` contains custom metadata passed to workflows.start()
|
|
2292
|
-
if (meta?.notify) {
|
|
2293
|
-
console.log("Apply completed for", meta.source);
|
|
2294
|
-
}
|
|
2295
|
-
},
|
|
2296
|
-
});
|
|
2297
|
-
```
|
|
2298
|
-
|
|
2299
|
-
**How it works:**
|
|
2300
|
-
|
|
2301
|
-
1. Workflow completes → server sets status to `apply_pending`
|
|
2302
|
-
2. All connected clients receive a `workflowStatus` event with `needsApply: true`
|
|
2303
|
-
3. The first client to call `claimApply` wins (conditional DynamoDB update)
|
|
2304
|
-
4. The claiming client runs the registered `onApply` handler
|
|
2305
|
-
5. On success, `confirmApply` marks the run as `completed`
|
|
2306
|
-
6. On failure, `releaseApply` releases the claim so another client can retry
|
|
2307
|
-
7. A 30-second lease timeout handles crashed clients
|
|
2308
|
-
8. On reconnect, the server automatically pushes any pending applies — no client-side polling needed
|
|
2309
|
-
|
|
2310
|
-
The claim/confirm/release cycle is handled automatically when you use `workflows.define()`. For manual control:
|
|
2311
|
-
|
|
2312
|
-
```typescript
|
|
2313
|
-
// Manual claim/confirm flow
|
|
2314
|
-
const claim = await client.workflows.claimApply({
|
|
2315
|
-
workflowKey: "my-workflow-key",
|
|
2316
|
-
runKey: "run-123",
|
|
2317
|
-
contextDocId: "doc-456",
|
|
2318
|
-
});
|
|
2319
|
-
|
|
2320
|
-
if (claim.claimed) {
|
|
2321
|
-
try {
|
|
2322
|
-
// Apply result to document...
|
|
2323
|
-
await client.workflows.confirmApply({
|
|
2324
|
-
workflowKey: "my-workflow-key",
|
|
2325
|
-
runKey: "run-123",
|
|
2326
|
-
contextDocId: "doc-456",
|
|
2327
|
-
});
|
|
2328
|
-
} catch (err) {
|
|
2329
|
-
// Release so another client can retry
|
|
2330
|
-
// (handled automatically with workflows.define)
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
```
|
|
2334
|
-
|
|
2335
|
-
### Sending File Attachments (PDFs, Images)
|
|
2336
|
-
|
|
2337
|
-
Workflows can process files like PDFs and images. Files must be base64-encoded before sending:
|
|
1957
|
+
- `runKey` makes the start idempotent per caller and `contextDocId`: repeating
|
|
1958
|
+
it answers the run that already exists, with `existing: true`, instead of
|
|
1959
|
+
starting a second one.
|
|
1960
|
+
- `waitFor` settles on `completed`, `failed`, or `terminated`. A failed run
|
|
1961
|
+
resolves with `status: "failed"` and its `error`; it does not reject.
|
|
1962
|
+
- `getStatus` is one read. A run id that is not this app's, or does not exist,
|
|
1963
|
+
rejects with `NOT_FOUND`.
|
|
2338
1964
|
|
|
2339
|
-
|
|
2340
|
-
/**
|
|
2341
|
-
* Load a file and convert to base64.
|
|
2342
|
-
* Works in browsers with fetch + FileReader or ArrayBuffer.
|
|
2343
|
-
*/
|
|
2344
|
-
async function loadFileAsBase64(url: string): Promise<string> {
|
|
2345
|
-
const response = await fetch(url);
|
|
2346
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
2347
|
-
const bytes = new Uint8Array(arrayBuffer);
|
|
2348
|
-
let binary = "";
|
|
2349
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2350
|
-
binary += String.fromCharCode(bytes[i]);
|
|
2351
|
-
}
|
|
2352
|
-
return btoa(binary);
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
// Load a PDF and send to workflow
|
|
2356
|
-
const pdfBase64 = await loadFileAsBase64("/path/to/document.pdf");
|
|
2357
|
-
|
|
2358
|
-
const result = await client.workflows.start({
|
|
2359
|
-
workflowKey: "extract-pdf-data",
|
|
2360
|
-
input: {
|
|
2361
|
-
attachments: [
|
|
2362
|
-
{
|
|
2363
|
-
data: pdfBase64,
|
|
2364
|
-
type: "application/pdf",
|
|
2365
|
-
},
|
|
2366
|
-
],
|
|
2367
|
-
},
|
|
2368
|
-
});
|
|
2369
|
-
```
|
|
1965
|
+
### Channels
|
|
2370
1966
|
|
|
2371
|
-
|
|
1967
|
+
A channel delivers what server functions publish to whoever has joined it. A
|
|
1968
|
+
function authorizes the caller for one channel with `ctx.channels.authorize`
|
|
1969
|
+
and returns the grant; the client presents it with `subscribeToChannel`, and
|
|
1970
|
+
every `ctx.channels.publish` to that channel arrives as a `channelMessage`
|
|
1971
|
+
event:
|
|
2372
1972
|
|
|
2373
1973
|
```typescript
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
const base64 = dataUrl.split(",")[1];
|
|
2381
|
-
resolve(base64);
|
|
2382
|
-
};
|
|
2383
|
-
reader.onerror = reject;
|
|
2384
|
-
reader.readAsDataURL(file);
|
|
2385
|
-
});
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
// Handle file input
|
|
2389
|
-
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
2390
|
-
fileInput.addEventListener("change", async () => {
|
|
2391
|
-
const file = fileInput.files?.[0];
|
|
2392
|
-
if (!file) return;
|
|
1974
|
+
// 1. Ask the authorizing function for a grant; it names this caller and one channel.
|
|
1975
|
+
const result = await client.functions.invoke<{ grant: string; expiresAt: number }>(
|
|
1976
|
+
"order-room",
|
|
1977
|
+
{ input: { orderId } }
|
|
1978
|
+
);
|
|
1979
|
+
if (result.status !== "completed" || !result.output) return;
|
|
2393
1980
|
|
|
2394
|
-
|
|
1981
|
+
// 2. Present the grant on the socket the client already has.
|
|
1982
|
+
const subscription = await client.subscribeToChannel(
|
|
1983
|
+
`orders:${orderId}`,
|
|
1984
|
+
result.output.grant
|
|
1985
|
+
);
|
|
2395
1986
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
attachments: [
|
|
2400
|
-
{
|
|
2401
|
-
data: base64Data,
|
|
2402
|
-
type: file.type, // e.g., "image/png", "application/pdf"
|
|
2403
|
-
filename: file.name,
|
|
2404
|
-
},
|
|
2405
|
-
],
|
|
2406
|
-
},
|
|
2407
|
-
});
|
|
1987
|
+
client.on("channelMessage", (event) => {
|
|
1988
|
+
// event.channel, event.payload, event.functionKey, event.sentAt
|
|
1989
|
+
console.log(event.channel, event.payload);
|
|
2408
1990
|
});
|
|
2409
|
-
```
|
|
2410
|
-
|
|
2411
|
-
#### Loading from URL (Node.js)
|
|
2412
|
-
|
|
2413
|
-
```typescript
|
|
2414
|
-
import * as fs from "fs";
|
|
2415
|
-
import * as path from "path";
|
|
2416
|
-
|
|
2417
|
-
function loadFileAsBase64Sync(filePath: string): string {
|
|
2418
|
-
const buffer = fs.readFileSync(filePath);
|
|
2419
|
-
return buffer.toString("base64");
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
const pdfPath = path.join(__dirname, "document.pdf");
|
|
2423
|
-
const pdfBase64 = loadFileAsBase64Sync(pdfPath);
|
|
2424
1991
|
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
{
|
|
2430
|
-
data: pdfBase64,
|
|
2431
|
-
type: "application/pdf",
|
|
2432
|
-
},
|
|
2433
|
-
],
|
|
2434
|
-
},
|
|
1992
|
+
// A grant that expired while the socket was down is refused on reconnect —
|
|
1993
|
+
// invoke the function again and subscribe again.
|
|
1994
|
+
client.on("channelSubscribeFailed", (event) => {
|
|
1995
|
+
console.warn("renew", event.channel, event.message);
|
|
2435
1996
|
});
|
|
2436
|
-
```
|
|
2437
|
-
|
|
2438
|
-
### Complete Example: PDF Processing Workflow
|
|
2439
|
-
|
|
2440
|
-
```typescript
|
|
2441
|
-
import { initializeClient } from "js-bao-wss-client";
|
|
2442
|
-
|
|
2443
|
-
async function processPDF(pdfUrl: string) {
|
|
2444
|
-
const client = await initializeClient({
|
|
2445
|
-
apiUrl: "https://api.example.com",
|
|
2446
|
-
wsUrl: "wss://ws.example.com",
|
|
2447
|
-
appId: "my-app",
|
|
2448
|
-
token: "jwt-token",
|
|
2449
|
-
databaseConfig: { type: "sqljs" },
|
|
2450
|
-
});
|
|
2451
|
-
|
|
2452
|
-
// Set up event listener for completion
|
|
2453
|
-
const completionPromise = new Promise<any>((resolve) => {
|
|
2454
|
-
client.on("workflowStatus", (event) => {
|
|
2455
|
-
if (event.status === "completed") {
|
|
2456
|
-
resolve(event.output);
|
|
2457
|
-
}
|
|
2458
|
-
});
|
|
2459
|
-
});
|
|
2460
|
-
|
|
2461
|
-
// Open a document to establish WebSocket connection
|
|
2462
|
-
const { metadata } = await client.documents.create({ title: "temp" });
|
|
2463
|
-
await client.documents.open(metadata.documentId);
|
|
2464
|
-
|
|
2465
|
-
// Load and encode the PDF
|
|
2466
|
-
const response = await fetch(pdfUrl);
|
|
2467
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
2468
|
-
const bytes = new Uint8Array(arrayBuffer);
|
|
2469
|
-
let binary = "";
|
|
2470
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2471
|
-
binary += String.fromCharCode(bytes[i]);
|
|
2472
|
-
}
|
|
2473
|
-
const pdfBase64 = btoa(binary);
|
|
2474
|
-
|
|
2475
|
-
// Start the workflow
|
|
2476
|
-
const result = await client.workflows.start({
|
|
2477
|
-
workflowKey: "extract-pdf-data",
|
|
2478
|
-
input: {
|
|
2479
|
-
attachments: [
|
|
2480
|
-
{
|
|
2481
|
-
data: pdfBase64,
|
|
2482
|
-
type: "application/pdf",
|
|
2483
|
-
},
|
|
2484
|
-
],
|
|
2485
|
-
},
|
|
2486
|
-
});
|
|
2487
1997
|
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
// Wait for completion (or poll with getStatus)
|
|
2491
|
-
const output = await completionPromise;
|
|
2492
|
-
console.log("Extracted data:", output);
|
|
2493
|
-
|
|
2494
|
-
// Cleanup
|
|
2495
|
-
await client.documents.delete(metadata.documentId);
|
|
2496
|
-
await client.destroy();
|
|
2497
|
-
|
|
2498
|
-
return output;
|
|
2499
|
-
}
|
|
1998
|
+
// Later: leave. Idempotent — the same as client.unsubscribeFromChannel(channel).
|
|
1999
|
+
subscription.unsubscribe();
|
|
2500
2000
|
```
|
|
2501
2001
|
|
|
2502
|
-
|
|
2002
|
+
A membership lasts until the grant's `expiresAt` (epoch milliseconds, also on
|
|
2003
|
+
the returned subscription). To keep listening, invoke the authorizing function
|
|
2004
|
+
again before then and call `subscribeToChannel` with the new grant; that
|
|
2005
|
+
replaces the membership rather than adding a second one.
|
|
2503
2006
|
|
|
2504
|
-
|
|
2007
|
+
### Deprecated Client APIs
|
|
2505
2008
|
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
client: JsBaoClient,
|
|
2509
|
-
workflowKey: string,
|
|
2510
|
-
runKey: string,
|
|
2511
|
-
timeoutMs = 60000,
|
|
2512
|
-
intervalMs = 2000
|
|
2513
|
-
): Promise<any> {
|
|
2514
|
-
const startTime = Date.now();
|
|
2515
|
-
|
|
2516
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
2517
|
-
const status = await client.workflows.getStatus({ workflowKey, runKey });
|
|
2009
|
+
These client APIs still work but are deprecated. Each has a server-function
|
|
2010
|
+
replacement:
|
|
2518
2011
|
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2012
|
+
- `client.workflows.*` — call a server function: `client.functions.invoke` for a result, or `client.functions.start` / `getStatus` / `waitFor` for a long-running run.
|
|
2013
|
+
- `client.prompts.execute` — call a server function that runs the prompt with `ctx.prompts.run`.
|
|
2014
|
+
- `client.integrations.call` — call a server function that calls the integration with `ctx.integrations.call`.
|
|
2015
|
+
- `client.databases.*` — call a server function that reads and writes records with `ctx.db` and manages databases with `ctx.api.databases`; for live updates, join a channel the function publishes to.
|
|
2016
|
+
- `client.cronTriggers.*` — schedule a server function by declaring `[[function.triggers.cron]]` in its config.
|
|
2522
2017
|
|
|
2523
|
-
|
|
2524
|
-
throw new Error(`Workflow failed: ${status.error}`);
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
if (status.status === "terminated") {
|
|
2528
|
-
throw new Error("Workflow was terminated");
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
// Still running, wait and retry
|
|
2532
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
throw new Error("Workflow timed out");
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
// Usage
|
|
2539
|
-
const result = await client.workflows.start({ workflowKey: "my-workflow", input: { data: "..." } });
|
|
2540
|
-
const output = await waitForCompletion(client, "my-workflow", result.runKey);
|
|
2541
|
-
```
|
|
2542
|
-
|
|
2543
|
-
## Online Databases
|
|
2544
|
-
|
|
2545
|
-
Online databases provide schemaless record storage with user permissions, groups, and group-based access control. All database operations are accessed through `client.databases`.
|
|
2546
|
-
|
|
2547
|
-
### Database Management
|
|
2548
|
-
|
|
2549
|
-
```typescript
|
|
2550
|
-
// Create a database
|
|
2551
|
-
const db = await client.databases.create({ title: "My Database" });
|
|
2552
|
-
console.log(db.databaseId, db.title);
|
|
2553
|
-
|
|
2554
|
-
// List databases accessible to the current user
|
|
2555
|
-
const databases = await client.databases.list();
|
|
2556
|
-
|
|
2557
|
-
// Get database details
|
|
2558
|
-
const info = await client.databases.get(databaseId);
|
|
2559
|
-
|
|
2560
|
-
// Delete a database (owner only)
|
|
2561
|
-
await client.databases.delete(databaseId);
|
|
2562
|
-
```
|
|
2563
|
-
|
|
2564
|
-
### Connecting to a Database (Record Engine)
|
|
2565
|
-
|
|
2566
|
-
Use `connect()` to get a `DoDb` record engine instance for reading and writing records:
|
|
2567
|
-
|
|
2568
|
-
```typescript
|
|
2569
|
-
const db = client.databases.connect(databaseId);
|
|
2570
|
-
|
|
2571
|
-
// Insert a record
|
|
2572
|
-
const record = await db.insert("contacts", { name: "Alice", email: "alice@example.com" });
|
|
2573
|
-
|
|
2574
|
-
// Query records
|
|
2575
|
-
const contacts = await db.query("contacts");
|
|
2576
|
-
|
|
2577
|
-
// Update a record
|
|
2578
|
-
await db.update("contacts", record.id, { email: "new@example.com" });
|
|
2579
|
-
|
|
2580
|
-
// Delete a record
|
|
2581
|
-
await db.delete("contacts", record.id);
|
|
2582
|
-
```
|
|
2583
|
-
|
|
2584
|
-
### Permissions
|
|
2585
|
-
|
|
2586
|
-
Control per-user access to databases. Only the database owner can add or remove managers.
|
|
2587
|
-
|
|
2588
|
-
```typescript
|
|
2589
|
-
// List permissions on a database
|
|
2590
|
-
const perms = await client.databases.listPermissions(databaseId);
|
|
2591
|
-
|
|
2592
|
-
// Add a user as a manager
|
|
2593
|
-
await client.databases.addManager(databaseId, {
|
|
2594
|
-
userId: "user-123",
|
|
2595
|
-
});
|
|
2596
|
-
|
|
2597
|
-
// Remove a manager
|
|
2598
|
-
await client.databases.removeManager(databaseId, "user-123");
|
|
2599
|
-
```
|
|
2600
|
-
|
|
2601
|
-
> **Deprecated:** `grantPermission()` and `revokePermission()` still work but are deprecated aliases for `addManager()` and `removeManager()`.
|
|
2602
|
-
|
|
2603
|
-
### Groups
|
|
2018
|
+
## Groups
|
|
2604
2019
|
|
|
2605
2020
|
Groups allow batch permission grants. A group has a `groupType` and `groupId`.
|
|
2606
2021
|
|
|
@@ -2648,90 +2063,6 @@ await client.groups.removeMember("team", "engineering", { email: "alice@example.
|
|
|
2648
2063
|
const memberships = await client.groups.listUserMemberships("user-123");
|
|
2649
2064
|
```
|
|
2650
2065
|
|
|
2651
|
-
### Group Permissions on Databases
|
|
2652
|
-
|
|
2653
|
-
Grant database access to an entire group instead of individual users:
|
|
2654
|
-
|
|
2655
|
-
```typescript
|
|
2656
|
-
// Grant a group permission on a database
|
|
2657
|
-
await client.databases.grantGroupPermission(databaseId, {
|
|
2658
|
-
groupType: "team",
|
|
2659
|
-
groupId: "engineering",
|
|
2660
|
-
permission: "read-write",
|
|
2661
|
-
});
|
|
2662
|
-
|
|
2663
|
-
// List group permissions
|
|
2664
|
-
const gperms = await client.databases.listGroupPermissions(databaseId);
|
|
2665
|
-
|
|
2666
|
-
// Revoke group permission
|
|
2667
|
-
await client.databases.revokeGroupPermission(databaseId, "team", "engineering");
|
|
2668
|
-
```
|
|
2669
|
-
|
|
2670
|
-
## Integrations API
|
|
2671
|
-
|
|
2672
|
-
Proxy HTTP calls through the tenant-specific integrations defined in the admin UI:
|
|
2673
|
-
|
|
2674
|
-
```typescript
|
|
2675
|
-
const response = await client.integrations.call({
|
|
2676
|
-
integrationKey: "weather-api",
|
|
2677
|
-
method: "GET",
|
|
2678
|
-
path: "/current",
|
|
2679
|
-
query: { city: "San Francisco" },
|
|
2680
|
-
headers: { "X-Debug": "true" },
|
|
2681
|
-
});
|
|
2682
|
-
|
|
2683
|
-
console.log(response.status); // Upstream status code
|
|
2684
|
-
console.log(response.body); // JSON returned by the provider
|
|
2685
|
-
console.log(response.traceId); // Proxy trace id (mirrors admin logs)
|
|
2686
|
-
console.log(response.durationMs); // Milliseconds spent in the worker
|
|
2687
|
-
```
|
|
2688
|
-
|
|
2689
|
-
- `integrationKey` – slug chosen in the admin UI. Keys are per-app.
|
|
2690
|
-
- `method`, `path`, `query`, `headers`, `body` – forwarded exactly as provided, but the worker enforces the integration’s allowlisted methods/paths, forwarded headers/query params, max body size, and timeout.
|
|
2691
|
-
- Success responses include the upstream `status`, `headers`, `body`, optional `traceId`, and `durationMs`.
|
|
2692
|
-
|
|
2693
|
-
### Errors
|
|
2694
|
-
|
|
2695
|
-
`client.integrations.call` throws `JsBaoError` with rich codes:
|
|
2696
|
-
|
|
2697
|
-
- `OFFLINE` – The SDK is forced offline (`networkMode = "offline"`).
|
|
2698
|
-
- `ACCESS_DENIED` – Missing/expired JWT.
|
|
2699
|
-
- `INTEGRATION_NOT_FOUND` – Integration removed/archived or typo in `integrationKey`.
|
|
2700
|
-
- `INTEGRATION_SECRET_MISSING` – Retired. Current servers never emit this code; it is retained for compatibility with older servers.
|
|
2701
|
-
- `INTEGRATION_REQUEST_INVALID` – Method/path/query/body violate guard rails (`DISALLOWED_METHOD`, `REQUEST_BODY_TOO_LARGE`, etc.).
|
|
2702
|
-
- `INTEGRATION_PROXY_FAILED` – Upstream timeout/5xx. Inspect `error.details.traceId` to correlate with admin logs.
|
|
2703
|
-
|
|
2704
|
-
Example handler:
|
|
2705
|
-
|
|
2706
|
-
```typescript
|
|
2707
|
-
import { isJsBaoError } from "js-bao-wss-client";
|
|
2708
|
-
|
|
2709
|
-
try {
|
|
2710
|
-
await client.integrations.call({ integrationKey: "crm", method: "POST", path: "/contacts", body: { email: "hi" } });
|
|
2711
|
-
} catch (error) {
|
|
2712
|
-
if (isJsBaoError(error)) {
|
|
2713
|
-
switch (error.code) {
|
|
2714
|
-
case "INTEGRATION_NOT_FOUND":
|
|
2715
|
-
alert("This integration is not available. Contact your admin.");
|
|
2716
|
-
break;
|
|
2717
|
-
case "INTEGRATION_SECRET_MISSING":
|
|
2718
|
-
alert("Configuration incomplete. Try again later.");
|
|
2719
|
-
break;
|
|
2720
|
-
case "INTEGRATION_REQUEST_INVALID":
|
|
2721
|
-
console.error("Request violates integration config", error.details);
|
|
2722
|
-
break;
|
|
2723
|
-
case "INTEGRATION_PROXY_FAILED":
|
|
2724
|
-
console.error("Upstream failed", error.details?.traceId);
|
|
2725
|
-
break;
|
|
2726
|
-
default:
|
|
2727
|
-
throw error;
|
|
2728
|
-
}
|
|
2729
|
-
} else {
|
|
2730
|
-
throw error;
|
|
2731
|
-
}
|
|
2732
|
-
}
|
|
2733
|
-
```
|
|
2734
|
-
|
|
2735
2066
|
## Real-time Collaboration
|
|
2736
2067
|
|
|
2737
2068
|
### Open and Edit Documents
|