js-bao-wss-client 2.0.3 → 2.0.5
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 +242 -39
- package/dist/JsBaoClient.d.ts +199 -16
- package/dist/JsBaoClient.d.ts.map +1 -1
- package/dist/JsBaoClient.js +240 -4
- package/dist/JsBaoClient.js.map +1 -1
- package/dist/api/blobBucketsApi.d.ts +29 -1
- package/dist/api/blobBucketsApi.d.ts.map +1 -1
- package/dist/api/blobBucketsApi.js +5 -3
- package/dist/api/blobBucketsApi.js.map +1 -1
- package/dist/api/collectionsApi.d.ts +9 -1
- package/dist/api/collectionsApi.d.ts.map +1 -1
- package/dist/api/collectionsApi.js.map +1 -1
- package/dist/api/databasesApi.d.ts +18 -1
- package/dist/api/databasesApi.d.ts.map +1 -1
- package/dist/api/databasesApi.js +8 -0
- package/dist/api/databasesApi.js.map +1 -1
- package/dist/api/documentsApi.d.ts +15 -5
- package/dist/api/documentsApi.d.ts.map +1 -1
- package/dist/api/documentsApi.js +19 -20
- package/dist/api/documentsApi.js.map +1 -1
- package/dist/api/geminiApi.d.ts.map +1 -1
- package/dist/api/geminiApi.js +11 -14
- package/dist/api/geminiApi.js.map +1 -1
- package/dist/api/groupsApi.d.ts +20 -4
- package/dist/api/groupsApi.d.ts.map +1 -1
- package/dist/api/groupsApi.js +9 -1
- package/dist/api/groupsApi.js.map +1 -1
- package/dist/api/notificationsApi.d.ts +134 -0
- package/dist/api/notificationsApi.d.ts.map +1 -0
- package/dist/api/notificationsApi.js +52 -0
- package/dist/api/notificationsApi.js.map +1 -0
- package/dist/api/resourceMetadataApi.d.ts +163 -0
- package/dist/api/resourceMetadataApi.d.ts.map +1 -0
- package/dist/api/resourceMetadataApi.js +56 -0
- package/dist/api/resourceMetadataApi.js.map +1 -0
- package/dist/browser.umd.js +514 -58
- package/dist/browser.umd.js.map +1 -1
- package/dist/errors.d.ts +34 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +56 -0
- package/dist/errors.js.map +1 -1
- package/dist/internal/authController.d.ts +10 -0
- package/dist/internal/authController.d.ts.map +1 -1
- package/dist/internal/authController.js +14 -2
- package/dist/internal/authController.js.map +1 -1
- package/dist/internal/httpClient.d.ts +8 -0
- package/dist/internal/httpClient.d.ts.map +1 -1
- package/dist/internal/httpClient.js +34 -5
- package/dist/internal/httpClient.js.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -345,6 +345,9 @@ async function testSync() {
|
|
|
345
345
|
# Core (always required)
|
|
346
346
|
npm install js-bao-wss-client yjs lib0 js-bao
|
|
347
347
|
|
|
348
|
+
# WebSocket transport (required in Node.js — the browser uses the global WebSocket)
|
|
349
|
+
npm install ws
|
|
350
|
+
|
|
348
351
|
# For SQLite storage (optional, recommended for persistence)
|
|
349
352
|
npm install better-sqlite3
|
|
350
353
|
|
|
@@ -352,7 +355,82 @@ npm install better-sqlite3
|
|
|
352
355
|
npm install y-sqlite3
|
|
353
356
|
```
|
|
354
357
|
|
|
355
|
-
Note: `better-sqlite3
|
|
358
|
+
Note: `ws`, `better-sqlite3`, and `y-sqlite3` are declared as **optional peer
|
|
359
|
+
dependencies**. In Node.js the client connects over WebSocket via `ws` (it
|
|
360
|
+
dynamically `import("ws")` when there is no global `WebSocket`), so `ws` must be
|
|
361
|
+
installed for any test or script that opens a document or database — otherwise
|
|
362
|
+
the connection attempt fails with `Cannot find package 'ws'`. `better-sqlite3`
|
|
363
|
+
and `y-sqlite3` are native modules requiring compilation; if you encounter build
|
|
364
|
+
issues, ensure you have the appropriate build tools installed (Python, C++
|
|
365
|
+
compiler). None are needed in the browser.
|
|
366
|
+
|
|
367
|
+
### Testing in Node.js (dual-context tests)
|
|
368
|
+
|
|
369
|
+
The same test can run in **two contexts** — interactively in an app's in-browser
|
|
370
|
+
test-harness panel, and headlessly in Node under a normal test runner (e.g.
|
|
371
|
+
[Vitest](https://vitest.dev)) to gate CI — because `JsBaoClient` runs the full
|
|
372
|
+
document / Yjs / database / blob lifecycle in Node with no browser. Author the
|
|
373
|
+
test once; run it in the panel while developing and under `vitest run` in CI.
|
|
374
|
+
|
|
375
|
+
**Authenticate from Node with no browser and no email delivery.** Use the
|
|
376
|
+
`+primitivetest` OTP bypass: `otpVerify(email, "000000")` authenticates and
|
|
377
|
+
connects a fresh test user, provided the email's base address is on the app's
|
|
378
|
+
`testAccountBaseEmails` whitelist (the app owner's per-app consent gate). No admin
|
|
379
|
+
credentials, no pre-existing user — the user is auto-provisioned on first verify.
|
|
380
|
+
|
|
381
|
+
Prerequisites:
|
|
382
|
+
|
|
383
|
+
- The app's `testAccountBaseEmails` includes the base of your sign-in email — set
|
|
384
|
+
it with `primitive apps update <appId> --test-account-bases <base@example.com>`.
|
|
385
|
+
The sign-in email is then any `<base>+primitivetest<suffix>@example.com`
|
|
386
|
+
derivative (e.g. `alice+primitivetest-ci@example.com`). Reuse a **stable
|
|
387
|
+
suffix** per CI project so the same test user is reused across runs (the
|
|
388
|
+
provisioner is find-or-create) rather than accumulating one user per run.
|
|
389
|
+
- `ws` is installed (see Required Packages above).
|
|
390
|
+
- Storage defaults to `auto` — SQLite via `better-sqlite3` when present, in-memory
|
|
391
|
+
otherwise. No native dependency is required to run ephemeral tests.
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { initializeClient } from "js-bao-wss-client";
|
|
395
|
+
|
|
396
|
+
// Author once — this same async body is what a browser harness test runs too.
|
|
397
|
+
const client = await initializeClient({
|
|
398
|
+
apiUrl: "https://your-api.example.com",
|
|
399
|
+
wsUrl: "wss://your-ws.example.com",
|
|
400
|
+
appId: "your-app-id",
|
|
401
|
+
models: [/* the models your tests need, as in the browser */],
|
|
402
|
+
// storageConfig defaults to { type: "auto" } — memory fallback in Node,
|
|
403
|
+
// no native deps required.
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// #1413 OTP bypass: whitelisted +primitivetest email + code "000000".
|
|
407
|
+
// On success the client is authenticated AND connected — no browser.
|
|
408
|
+
await client.otpVerify("alice+primitivetest-ci@example.com", "000000");
|
|
409
|
+
|
|
410
|
+
// From here the client behaves exactly as in the browser.
|
|
411
|
+
const doc = await client.documents.create();
|
|
412
|
+
await client.documents.open(doc.id);
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Under `vitest run`, a failing assertion or a group that throws surfaces as a test
|
|
416
|
+
failure and yields a **non-zero exit** natively (all passing → exit `0`), so the
|
|
417
|
+
suite gates a merge with no bespoke runner. Emit JUnit for CI with
|
|
418
|
+
`vitest run --reporter=junit`.
|
|
419
|
+
|
|
420
|
+
**Failure mode.** If the sign-in email is **not** whitelisted (its base is absent
|
|
421
|
+
from `testAccountBaseEmails`, or the app's mode rejects the provisioning),
|
|
422
|
+
`otpVerify` rejects with a clear authentication error rather than hanging — catch
|
|
423
|
+
it and fail the test:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
await expect(
|
|
427
|
+
client.otpVerify("not-whitelisted@example.com", "000000"),
|
|
428
|
+
).rejects.toThrow(/otp|auth|verif/i);
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
> The in-browser test-harness panel and this Node path run the **same registered
|
|
432
|
+
> tests**. Nothing in the test authoring contract requires a DOM — every browser
|
|
433
|
+
> API the client touches is guarded with `typeof window === "undefined"`.
|
|
356
434
|
|
|
357
435
|
## Quick Start
|
|
358
436
|
|
|
@@ -1092,14 +1170,12 @@ await client.documents.delete(documentId, { forceCloseIfOpen: true }); // auto-c
|
|
|
1092
1170
|
|
|
1093
1171
|
### Document Aliases
|
|
1094
1172
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
aliasKey: "home",
|
|
1100
|
-
documentId,
|
|
1101
|
-
});
|
|
1173
|
+
> **Note:** Only `scope: "user"` is supported. App-scoped aliases were removed
|
|
1174
|
+
> in #1168 — they conferred no access (there is no app-wide document-sharing
|
|
1175
|
+
> primitive) and only named a document every caller had to already be permitted
|
|
1176
|
+
> to open.
|
|
1102
1177
|
|
|
1178
|
+
```typescript
|
|
1103
1179
|
// User-scoped alias (defaults userId to the current user)
|
|
1104
1180
|
await client.documents.aliases.set({
|
|
1105
1181
|
scope: "user",
|
|
@@ -1109,21 +1185,21 @@ await client.documents.aliases.set({
|
|
|
1109
1185
|
|
|
1110
1186
|
// Resolve an alias
|
|
1111
1187
|
const alias = await client.documents.aliases.resolve({
|
|
1112
|
-
scope: "
|
|
1113
|
-
aliasKey: "
|
|
1188
|
+
scope: "user",
|
|
1189
|
+
aliasKey: "current-draft",
|
|
1114
1190
|
});
|
|
1115
1191
|
|
|
1116
1192
|
// Open a document via alias (same return shape as documents.open)
|
|
1117
1193
|
const { doc } = await client.documents.openAlias({
|
|
1118
|
-
scope: "
|
|
1119
|
-
aliasKey: "
|
|
1194
|
+
scope: "user",
|
|
1195
|
+
aliasKey: "current-draft",
|
|
1120
1196
|
});
|
|
1121
1197
|
|
|
1122
1198
|
// List aliases for a document (requires document access)
|
|
1123
1199
|
const aliases = await client.documents.aliases.listForDocument(documentId);
|
|
1124
1200
|
|
|
1125
1201
|
// Delete an alias (no error if already missing)
|
|
1126
|
-
await client.documents.aliases.delete({ scope: "
|
|
1202
|
+
await client.documents.aliases.delete({ scope: "user", aliasKey: "current-draft" });
|
|
1127
1203
|
```
|
|
1128
1204
|
|
|
1129
1205
|
#### Atomic Create with Alias
|
|
@@ -1131,21 +1207,8 @@ await client.documents.aliases.delete({ scope: "app", aliasKey: "home" });
|
|
|
1131
1207
|
Create a document and alias in a single atomic operation. This is an **online-only** operation that only creates the document if the alias doesn't already exist:
|
|
1132
1208
|
|
|
1133
1209
|
```typescript
|
|
1134
|
-
// Create document with
|
|
1210
|
+
// Create document with a user-scoped alias
|
|
1135
1211
|
const result = await client.documents.createWithAlias({
|
|
1136
|
-
title: "Home Page",
|
|
1137
|
-
alias: {
|
|
1138
|
-
scope: "app",
|
|
1139
|
-
aliasKey: "home",
|
|
1140
|
-
},
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
console.log(result.documentId); // The created document ID
|
|
1144
|
-
console.log(result.alias.aliasKey); // "home"
|
|
1145
|
-
console.log(result.alias.documentId); // Same as documentId
|
|
1146
|
-
|
|
1147
|
-
// Create document with user-scoped alias
|
|
1148
|
-
const userResult = await client.documents.createWithAlias({
|
|
1149
1212
|
title: "My Draft",
|
|
1150
1213
|
alias: {
|
|
1151
1214
|
scope: "user",
|
|
@@ -1153,11 +1216,15 @@ const userResult = await client.documents.createWithAlias({
|
|
|
1153
1216
|
},
|
|
1154
1217
|
});
|
|
1155
1218
|
|
|
1156
|
-
//
|
|
1219
|
+
console.log(result.documentId); // The created document ID
|
|
1220
|
+
console.log(result.alias.aliasKey); // "current-draft"
|
|
1221
|
+
console.log(result.alias.documentId); // Same as documentId
|
|
1222
|
+
|
|
1223
|
+
// Attempting to create with an existing alias throws HTTP 409
|
|
1157
1224
|
try {
|
|
1158
1225
|
await client.documents.createWithAlias({
|
|
1159
|
-
title: "Another
|
|
1160
|
-
alias: { scope: "
|
|
1226
|
+
title: "Another Draft",
|
|
1227
|
+
alias: { scope: "user", aliasKey: "current-draft" },
|
|
1161
1228
|
});
|
|
1162
1229
|
} catch (err) {
|
|
1163
1230
|
console.log("Alias already exists");
|
|
@@ -1988,6 +2055,67 @@ const status = await client.workflows.getStatus({
|
|
|
1988
2055
|
|
|
1989
2056
|
If `contextDocId` is omitted, the user's root document is used by default.
|
|
1990
2057
|
|
|
2058
|
+
### Waiting for Workflow Completion
|
|
2059
|
+
|
|
2060
|
+
For an async run, `waitFor(runId)` resolves once the run reaches a terminal
|
|
2061
|
+
state — no manual subscribe/poll/timeout loop:
|
|
2062
|
+
|
|
2063
|
+
```typescript
|
|
2064
|
+
const { runId } = await client.workflows.start({
|
|
2065
|
+
workflowKey: "process-document",
|
|
2066
|
+
input: { action: "analyze" },
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
const result = await client.workflows.waitFor(runId);
|
|
2070
|
+
// result: { status, output?, error? }
|
|
2071
|
+
|
|
2072
|
+
if (result.status === "completed") {
|
|
2073
|
+
console.log("Output:", result.output);
|
|
2074
|
+
} else if (result.status === "failed") {
|
|
2075
|
+
console.log("Error:", result.error); // a failed run RESOLVES — it does not reject
|
|
2076
|
+
}
|
|
2077
|
+
```
|
|
2078
|
+
|
|
2079
|
+
`waitFor` is event-driven: it subscribes to the same `workflowStatus`
|
|
2080
|
+
WebSocket frame (no polling) and does a single reconcile fetch right after
|
|
2081
|
+
subscribing to catch a run that already finished before you called it. On a
|
|
2082
|
+
WebSocket reconnect it re-runs that single reconcile — there is no interval
|
|
2083
|
+
timer.
|
|
2084
|
+
|
|
2085
|
+
- **Resolves on every terminal state**: `completed`, `failed`, `terminated`,
|
|
2086
|
+
and the apply-ready states `apply_pending` / `apply_claimed` (the run
|
|
2087
|
+
finished and its result is available for the client-side apply step). It also
|
|
2088
|
+
treats any run whose `endedAt` is set as terminal regardless of its reported
|
|
2089
|
+
`status`, so it never hangs on a stale status.
|
|
2090
|
+
- **Rejects only** on two conditions: the run is unknown or not readable by the
|
|
2091
|
+
caller (a `NOT_FOUND` `JsBaoError`, surfaced immediately — it does not wait
|
|
2092
|
+
for the timeout), or the timeout elapses (a `WORKFLOW_WAIT_TIMEOUT`
|
|
2093
|
+
`JsBaoError`).
|
|
2094
|
+
|
|
2095
|
+
```typescript
|
|
2096
|
+
import { isJsBaoError } from "js-bao-wss-client";
|
|
2097
|
+
|
|
2098
|
+
try {
|
|
2099
|
+
const result = await client.workflows.waitFor(runId, { timeoutMs: 60000 });
|
|
2100
|
+
// ...
|
|
2101
|
+
} catch (err) {
|
|
2102
|
+
if (isJsBaoError(err) && err.code === "WORKFLOW_WAIT_TIMEOUT") {
|
|
2103
|
+
// still running after the deadline
|
|
2104
|
+
} else if (isJsBaoError(err) && err.code === "NOT_FOUND") {
|
|
2105
|
+
// unknown or not-owned runId
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
```
|
|
2109
|
+
|
|
2110
|
+
The default `timeoutMs` is 15 minutes. Set it to `0` or `Infinity` to disable
|
|
2111
|
+
the timeout — but then the returned promise and its listener live until the run
|
|
2112
|
+
terminates, so only do this for runs you know will end.
|
|
2113
|
+
|
|
2114
|
+
To receive the resolving `workflowStatus` frame promptly you need an active
|
|
2115
|
+
WebSocket connection (see [Listening for Workflow
|
|
2116
|
+
Events](#listening-for-workflow-events) below); without one, `waitFor` still
|
|
2117
|
+
resolves via its reconcile fetch, just less immediately.
|
|
2118
|
+
|
|
1991
2119
|
### Listening for Workflow Events
|
|
1992
2120
|
|
|
1993
2121
|
Subscribe to real-time workflow events via WebSocket:
|
|
@@ -2054,7 +2182,7 @@ const runs = await client.workflows.listRuns();
|
|
|
2054
2182
|
console.log("Total runs:", runs.items.length);
|
|
2055
2183
|
|
|
2056
2184
|
runs.items.forEach((run) => {
|
|
2057
|
-
console.log(run.runKey, run.status, run.
|
|
2185
|
+
console.log(run.runKey, run.status, run.startedAt);
|
|
2058
2186
|
});
|
|
2059
2187
|
|
|
2060
2188
|
// Filter by workflow
|
|
@@ -2097,6 +2225,15 @@ const combined = await client.workflows.listRuns({
|
|
|
2097
2225
|
#### Run Record Fields
|
|
2098
2226
|
|
|
2099
2227
|
```typescript
|
|
2228
|
+
type WorkflowRunStatus =
|
|
2229
|
+
| "queued"
|
|
2230
|
+
| "running"
|
|
2231
|
+
| "completed"
|
|
2232
|
+
| "failed"
|
|
2233
|
+
| "terminated"
|
|
2234
|
+
| "apply_pending"
|
|
2235
|
+
| "apply_claimed";
|
|
2236
|
+
|
|
2100
2237
|
interface WorkflowRun {
|
|
2101
2238
|
runId: string;
|
|
2102
2239
|
runKey: string;
|
|
@@ -2105,9 +2242,11 @@ interface WorkflowRun {
|
|
|
2105
2242
|
workflowKey: string;
|
|
2106
2243
|
revisionId: string;
|
|
2107
2244
|
contextDocId?: string;
|
|
2108
|
-
status:
|
|
2109
|
-
|
|
2245
|
+
status: WorkflowRunStatus;
|
|
2246
|
+
startedAt?: string; // ISO timestamp stamped when the run started
|
|
2110
2247
|
endedAt?: string;
|
|
2248
|
+
errorMessage?: string | null; // set when status is "failed", null otherwise
|
|
2249
|
+
meta?: Record<string, any>; // custom metadata passed to workflows.start()
|
|
2111
2250
|
}
|
|
2112
2251
|
```
|
|
2113
2252
|
|
|
@@ -2933,21 +3072,85 @@ console.log("Last activity:", session.lastActivity);
|
|
|
2933
3072
|
|
|
2934
3073
|
## Error Handling
|
|
2935
3074
|
|
|
3075
|
+
HTTP operations throw **typed errors** so you can branch on the kind of failure
|
|
3076
|
+
without parsing message strings:
|
|
3077
|
+
|
|
3078
|
+
- **`JsBaoApiError`** — the server responded with a non-2xx status. Carries:
|
|
3079
|
+
- `status: number` — the HTTP status code (always present).
|
|
3080
|
+
- `code?: string` — a stable, machine-readable code from the server body
|
|
3081
|
+
(e.g. `"alias_exists"` on a 409), or `undefined` when the server didn't
|
|
3082
|
+
supply one.
|
|
3083
|
+
- `body?: unknown` — the parsed JSON error body
|
|
3084
|
+
(`{ error, status, timestamp, code?, details? }`), the raw text for a
|
|
3085
|
+
non-JSON body, or `null` for an empty body.
|
|
3086
|
+
- `message` — keeps the legacy `HTTP <status>: <text>` prefix, so older code
|
|
3087
|
+
that regex-parses `.message` keeps working.
|
|
3088
|
+
- **`JsBaoNetworkError`** — the request never reached the server (unreachable
|
|
3089
|
+
host, refused connection, DNS failure, abort). A network failure is
|
|
3090
|
+
inherently retryable; decide retries with `instanceof JsBaoNetworkError`.
|
|
3091
|
+
- **`JsBaoError`** — client-side logical errors with a `code` (e.g. `"OFFLINE"`
|
|
3092
|
+
when the client is in offline mode). Unchanged.
|
|
3093
|
+
|
|
3094
|
+
Use the guards `isJsBaoApiError` / `isJsBaoNetworkError` / `isJsBaoError`, or
|
|
3095
|
+
`instanceof`. Neither `JsBaoApiError` nor `JsBaoNetworkError` exposes a
|
|
3096
|
+
`retryable` field — the class already tells you whether a retry makes sense.
|
|
3097
|
+
|
|
2936
3098
|
```typescript
|
|
3099
|
+
import {
|
|
3100
|
+
JsBaoApiError,
|
|
3101
|
+
JsBaoNetworkError,
|
|
3102
|
+
isJsBaoApiError,
|
|
3103
|
+
isJsBaoNetworkError,
|
|
3104
|
+
} from "js-bao-wss-client";
|
|
3105
|
+
|
|
2937
3106
|
try {
|
|
2938
|
-
// HTTP operations throw exceptions
|
|
2939
3107
|
const res = await client.documents.create({ title: "Test" });
|
|
2940
3108
|
} catch (error) {
|
|
2941
|
-
if ((error
|
|
2942
|
-
console.error("
|
|
2943
|
-
} else if ((error
|
|
2944
|
-
|
|
3109
|
+
if (isJsBaoNetworkError(error)) {
|
|
3110
|
+
console.error("Server unreachable — safe to retry");
|
|
3111
|
+
} else if (isJsBaoApiError(error)) {
|
|
3112
|
+
switch (error.status) {
|
|
3113
|
+
case 401:
|
|
3114
|
+
console.error("Authentication failed");
|
|
3115
|
+
break;
|
|
3116
|
+
case 403:
|
|
3117
|
+
console.error("Permission denied");
|
|
3118
|
+
break;
|
|
3119
|
+
case 404:
|
|
3120
|
+
console.error("Not found");
|
|
3121
|
+
break;
|
|
3122
|
+
default:
|
|
3123
|
+
console.error(`API error ${error.status} (${error.code ?? "no code"})`);
|
|
3124
|
+
}
|
|
2945
3125
|
} else if ((error as any).code === "OFFLINE") {
|
|
2946
3126
|
console.error("Client offline");
|
|
2947
3127
|
} else {
|
|
2948
|
-
|
|
3128
|
+
throw error;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
```
|
|
3132
|
+
|
|
3133
|
+
### Idempotent add/remove without string parsing
|
|
3134
|
+
|
|
3135
|
+
Branch on `status`/`code` to treat an expected conflict as success:
|
|
3136
|
+
|
|
3137
|
+
```typescript
|
|
3138
|
+
// Adding an item that already exists (409) is success for an idempotent add.
|
|
3139
|
+
async function addOnce() {
|
|
3140
|
+
try {
|
|
3141
|
+
await client.makeRequest("PUT", `/document-aliases/app/${key}`, {
|
|
3142
|
+
documentId,
|
|
3143
|
+
mustNotExist: true,
|
|
3144
|
+
});
|
|
3145
|
+
return "created";
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
if (isJsBaoApiError(error) && error.status === 409) {
|
|
3148
|
+
return "already-exists"; // idempotent — not a real failure
|
|
3149
|
+
}
|
|
3150
|
+
throw error;
|
|
2949
3151
|
}
|
|
2950
3152
|
}
|
|
3153
|
+
```
|
|
2951
3154
|
|
|
2952
3155
|
// WebSocket errors are handled via events
|
|
2953
3156
|
client.on("connection-error", (error) => {
|