js-bao-wss-client 2.2.0-alpha.13 → 2.2.0-alpha.14

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 CHANGED
@@ -1280,15 +1280,19 @@ console.log(result.documentId); // The created document ID
1280
1280
  console.log(result.alias.aliasKey); // "current-draft"
1281
1281
  console.log(result.alias.documentId); // Same as documentId
1282
1282
 
1283
- // The alias create takes `documentFormat` too, so a large document can be
1284
- // created with its alias atomically. The response carries `documentFormat: 2`
1285
- // for a large document and omits the key for an ordinary one.
1283
+ // The alias create takes the whole create vocabulary — `tags`, `metadata` and
1284
+ // `documentFormat` — with the same meaning they have on `create()`: applied when
1285
+ // the call creates the document. The response echoes what it applied, and omits
1286
+ // `documentFormat` for an ordinary document.
1286
1287
  const ledger = await client.documents.createWithAlias({
1287
1288
  title: "Ledger",
1288
1289
  alias: { scope: "user", aliasKey: "ledger" },
1290
+ tags: ["ledger", "q3"],
1291
+ metadata: { owner: "finance" },
1289
1292
  documentFormat: 2,
1290
1293
  });
1291
1294
  console.log(ledger.documentFormat); // 2
1295
+ console.log(ledger.tags); // ["ledger", "q3"]
1292
1296
 
1293
1297
  // Attempting to create with an existing alias throws HTTP 409
1294
1298
  try {
@@ -1310,6 +1314,27 @@ try {
1310
1314
 
1311
1315
  Use `createWithAlias()` when you need guaranteed uniqueness based on an alias (e.g., "only one home page per app"). Use regular `create()` + `aliases.set()` when offline support is needed or when the document should be created regardless of alias conflicts.
1312
1316
 
1317
+ #### Get-or-Create with Alias
1318
+
1319
+ When you want the document to exist rather than to be new — "this user's ledger" — `getOrCreateWithAlias()` is the idempotent form: the first call creates it, every later one returns the same document. It takes the same options, applied only when the call creates the document:
1320
+
1321
+ ```typescript
1322
+ const ledger = await client.documents.getOrCreateWithAlias({
1323
+ title: "Ledger",
1324
+ alias: { scope: "user", aliasKey: "ledger" },
1325
+ tags: ["ledger"],
1326
+ documentFormat: 2,
1327
+ });
1328
+
1329
+ console.log(ledger.created); // true on the call that made it, false after
1330
+ console.log(ledger.documentFormat); // 2 either way
1331
+ console.log(ledger.tags); // ["ledger"] on the create; absent afterwards
1332
+ ```
1333
+
1334
+ A stated `documentFormat` is also a statement about a document the alias already names. If the stored format **differs**, the call is refused with `DOCUMENT_FORMAT_MISMATCH` (409) naming the document and both formats, and nothing is created — being handed an ordinary document when you asked for a large one is the mistake this route exists to prevent. If it agrees, the format is echoed. Omit it and an existing binding is answered exactly as it always was.
1335
+
1336
+ > All three create routes refuse a top-level body key they do not read, with 400 `VALIDATION_FAILED` and a `details` entry naming it. The typed methods above send only keys the routes read; this reaches you only if you build a request body by hand.
1337
+
1313
1338
  ### Manage Permissions
1314
1339
 
1315
1340
  ```typescript
@@ -15,6 +15,7 @@ import { type RequestOptions } from "./internal/httpClient";
15
15
  import { type DocumentPermission as DocumentAccessLevel, type LocalDocumentEntry, type LocalMetadataEntry, type DocumentDebugSnapshot } from "./internal/documentManager";
16
16
  import { type OfflineReplayNotice } from "js-bao";
17
17
  import type { DocumentOfflineWindowError } from "js-bao";
18
+ import { JsBaoError } from "./errors";
18
19
  import { DocumentsAPI, DocumentContext, type DocumentInfo, type ResolveAliasParams, type CreateDocumentOptions } from "./api/documentsApi";
19
20
  import { type CacheFacade } from "./api/cacheFacade";
20
21
  import { MeAPI } from "./api/meApi";
@@ -407,6 +408,25 @@ export interface ExecutePromptResult<R = unknown> {
407
408
  success: boolean;
408
409
  output: string;
409
410
  error?: string;
411
+ /**
412
+ * The upstream provider's own HTTP status, on a failure where the call
413
+ * reached the provider and it answered. It does not depend on which provider
414
+ * the prompt's configuration names, and it is what a retry decision is made
415
+ * on without parsing `error`: retry on 408, 429, 502, 503 and 504; any other
416
+ * 4xx will fail the same way next time.
417
+ *
418
+ * Absent on success, and absent on a failure where no provider answer was
419
+ * observed — an unset provider key, an oversized payload, a completion that
420
+ * came back empty — so a number here is always the provider's own.
421
+ */
422
+ upstreamStatus?: number;
423
+ /**
424
+ * `"PROMPT_UPSTREAM_TIMEOUT"` when the provider itself ran out of time,
425
+ * which is the one failure of the model call that carries a code. Absent on
426
+ * success and on every other provider failure, so `error` with no
427
+ * `errorCode` still means "the call failed for some other reason".
428
+ */
429
+ errorCode?: "PROMPT_UPSTREAM_TIMEOUT";
410
430
  metrics: {
411
431
  durationMs: number;
412
432
  inputTokens?: number;
@@ -663,6 +683,36 @@ export interface DocumentWriteRefusedEvent {
663
683
  */
664
684
  error: DocumentOfflineWindowError;
665
685
  }
686
+ /**
687
+ * Payload of the `document:format-mismatch` event
688
+ * (`client.on("document:format-mismatch", ...)`).
689
+ *
690
+ * Fired when the platform refuses a document because the format this client
691
+ * believed it had is not the format the platform resolved. The document is
692
+ * CLOSED when this arrives — its local rows and its unacknowledged writes are
693
+ * kept, nothing is evicted — because two of the three open shapes have no
694
+ * waiter to reject: a cached open sends its handshake from a microtask, and an
695
+ * already-open document re-syncs on reconnect. An open that WAS waiting on the
696
+ * network rejects with the same error as well.
697
+ *
698
+ * Diagnose from {@link declared} and {@link actual} before doing anything: the
699
+ * server also logs `Document format disagreement` naming its own pinned format.
700
+ * Evicting the document clears this client's belief, which repairs a stale row
701
+ * and HIDES a mis-pinned document.
702
+ */
703
+ export interface DocumentFormatMismatchEvent {
704
+ documentId: string;
705
+ /** What this client believed, from its own local metadata row. */
706
+ declared: 1 | 2;
707
+ /** What the platform resolved for the document. */
708
+ actual: 1 | 2;
709
+ /**
710
+ * The very error a waiting `documents.open` rejects with: `code` is
711
+ * `DOCUMENT_FORMAT_MISMATCH` and `details` carries `documentId`, `declared`
712
+ * and `actual`.
713
+ */
714
+ error: JsBaoError;
715
+ }
666
716
  /**
667
717
  * Options for `client.syncMetadata()`. Controls whether the sync covers all
668
718
  * of the user's document metadata or a single document (`scope` /
@@ -1794,6 +1844,7 @@ export interface JsBaoEvents {
1794
1844
  awareness: AwarenessEvent;
1795
1845
  "document:snapshot-load": DocumentSnapshotLoadEvent;
1796
1846
  "document:write-refused": DocumentWriteRefusedEvent;
1847
+ "document:format-mismatch": DocumentFormatMismatchEvent;
1797
1848
  "blobs:upload-progress": BlobUploadProgressEvent;
1798
1849
  "blobs:upload-completed": BlobUploadCompletedEvent;
1799
1850
  "blobs:upload-failed": BlobUploadFailedEvent;
@@ -2319,6 +2370,28 @@ export declare class JsBaoClient extends Observable<any> {
2319
2370
  * and released once the handshake has been answered.
2320
2371
  */
2321
2372
  private readonly format2AwaitingHandshake;
2373
+ /**
2374
+ * The format each open attempt in flight pinned — #3764, decision D8.
2375
+ *
2376
+ * One format per open, declared on `syncStep1` and BOUND by the ORM. The
2377
+ * background document read `DocumentManager.openDocument` starts rewrites the
2378
+ * metadata row in between, so reading the row at the bind could bind the
2379
+ * other format after the handshake had agreed on this one — the exact failure
2380
+ * this feature exists to prevent. Cleared when the open settles, after which
2381
+ * the row is the source again.
2382
+ */
2383
+ private readonly openAttemptFormats;
2384
+ /**
2385
+ * Documents the platform has REFUSED on this connection — #3764.
2386
+ *
2387
+ * A refused document sends no further handshake, is not retried by the sync
2388
+ * watchdog, and does not rebuild the socket on its account; a fresh open of it
2389
+ * is rejected with the refusal rather than waiting out an availability budget
2390
+ * for a handshake this client will not send. The mark is cleared when the
2391
+ * document is closed, evicted, or its metadata row's format changes — after
2392
+ * any of which this client's belief is no longer the one that was refused.
2393
+ */
2394
+ private readonly format2FormatRefusals;
2322
2395
  /** Outbound sequences per large document: queued, sent, acknowledged. */
2323
2396
  private readonly format2Ack;
2324
2397
  private jsBaoAddDocumentModelMapping;
@@ -3398,6 +3471,12 @@ export declare class JsBaoClient extends Observable<any> {
3398
3471
  * the server's mark is contiguous, so nothing above a covered sequence can be
3399
3472
  * acknowledged anyway, and claiming it would only invite the prune this
3400
3473
  * exists to prevent.
3474
+ *
3475
+ * A `legacy` note (#3760) is the same debt reached a different way — writes
3476
+ * taken over from a pre-amendment pool and kept off the overlay until they
3477
+ * are judged — so it withholds exactly as an ordinary one does (finding
3478
+ * 3760-REVIEW-07). Only the bulk-load kind is left out: `_discontinuities`
3479
+ * owns that debt and its own hold.
3401
3480
  */
3402
3481
  private format2ApplyDurableWithhold;
3403
3482
  /** The withheld writes have been replayed: their sequences may travel. */
@@ -3848,6 +3927,95 @@ export declare class JsBaoClient extends Observable<any> {
3848
3927
  * nothing of its local state may go out (see `format2RequireReload`).
3849
3928
  */
3850
3929
  private format2NeedsReload;
3930
+ /**
3931
+ * Pin what this open attempt BELIEVES the document's format is, before any
3932
+ * handshake can leave (#3764, D8).
3933
+ *
3934
+ * Deliberately the local row's own answer rather than `_resolveDocumentFormat`,
3935
+ * and the difference is the whole point twice over:
3936
+ *
3937
+ * - that resolver DEFAULTS. It answers 1 for a document nobody knows about,
3938
+ * which is the right thing to BIND (the compatible layout) and a false
3939
+ * thing to DECLARE — a first open of somebody else's document would claim
3940
+ * format 1 and be refused. Only a row that NAMES a format is a belief, and
3941
+ * `documentFormatOf` answers `null` for anything else (criterion 3).
3942
+ * - it can reach the SERVER, and this runs before the connection is up, so
3943
+ * resolving here would both add a request to the open's critical path and
3944
+ * answer differently depending on how far the connect had got.
3945
+ *
3946
+ * Nothing is pinned when nothing is believed, and the bind then resolves as it
3947
+ * always has. When something IS pinned, the declaration and the bind are that
3948
+ * one value — which is what makes a row rewritten in between unable to move
3949
+ * the bind under a handshake that has already agreed.
3950
+ */
3951
+ private pinOpenAttemptFormat;
3952
+ /**
3953
+ * The format the ORM binds this document on (#3764, D8).
3954
+ *
3955
+ * The attempt's pinned answer when an open is in flight — never a second
3956
+ * resolution, which is what let a row rewritten mid-open move the bind under a
3957
+ * handshake that had already agreed. With no attempt (a rebind outside an
3958
+ * open) the ordinary resolution stands.
3959
+ */
3960
+ private boundDocumentFormat;
3961
+ /**
3962
+ * The format this client DECLARES for a document on `syncStep1` (#3764).
3963
+ *
3964
+ * The attempt's pinned answer while an open is in flight, else what the local
3965
+ * metadata row says. `undefined` — a first open of somebody else's document,
3966
+ * or one whose row an eviction removed — declares nothing, and such a client
3967
+ * is served exactly as it was before this field existed (criterion 3).
3968
+ */
3969
+ private declaredDocumentFormat;
3970
+ /**
3971
+ * Whether the platform has refused this document on this connection (#3764).
3972
+ *
3973
+ * The recorded belief is re-read before the mark is: behavior 17's rule is
3974
+ * that the mark clears when the row's format changes, and a row is written
3975
+ * from more places than the format resolver's own `remember` — a listing
3976
+ * upgrades it, and so does the canonical read an open performs. Asking here
3977
+ * means no write site has to remember, and the answer cannot drift from what
3978
+ * the next handshake would declare.
3979
+ */
3980
+ private formatRefusalFor;
3981
+ /**
3982
+ * Reject an open whose document the platform refused while it ran (#3764).
3983
+ *
3984
+ * A cached open resolves without a waiter, so a refusal landing while it is
3985
+ * still binding the ORM has nothing to fail. Checked after the bind and
3986
+ * before the handle is returned, the open rejects with the typed error
3987
+ * instead of handing back a document whose sync and persistence the
3988
+ * refusal's teardown has detached. A mark that stood before the open began
3989
+ * never gets here: the open refuses it on entry.
3990
+ */
3991
+ private rejectIfRefusedDuringOpen;
3992
+ /** Drop a format refusal, naming why it no longer applies (#3764). */
3993
+ private clearFormatRefusal;
3994
+ /**
3995
+ * A document's locally recorded format changed, so the belief that was
3996
+ * refused is no longer the one this client holds (#3764, behavior 17).
3997
+ *
3998
+ * Only a change AWAY from the refused declaration clears the mark: a row that
3999
+ * still says the refused thing would only be refused again.
4000
+ */
4001
+ private noteDocumentFormatChanged;
4002
+ /**
4003
+ * Refuse ONE document because the platform and this client disagree about its
4004
+ * format — #3764, decision D7.
4005
+ *
4006
+ * A document-scoped teardown rather than a waiter failer, and that is the
4007
+ * whole finding: of the three shapes an open takes, only a network wait has a
4008
+ * waiter. A cached open sends its `syncStep1` from a microtask with nothing
4009
+ * waiting, and an already-open document re-syncs on reconnect — so an
4010
+ * event-only answer would leave both bound and syncing against a document the
4011
+ * platform is refusing to serve them.
4012
+ *
4013
+ * What it does NOT do: close the connection (D1 — one document is refused,
4014
+ * and the socket's other documents are none of its business) and clear any
4015
+ * local data (the rows and `_pending_ops` are kept, so a corrected open
4016
+ * publishes what this client wrote).
4017
+ */
4018
+ private refuseDocumentFormat;
3851
4019
  /**
3852
4020
  * Hold every open large document's queued writes until its own handshake on
3853
4021
  * this connection has been answered (#3435).