js-bao 0.5.0 → 0.5.2

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/dist/client.cjs CHANGED
@@ -84,35 +84,82 @@ var init_DocumentQueryTranslator = __esm({
84
84
  }
85
85
  });
86
86
 
87
+ // src/utils/yjsValues.ts
88
+ var Y;
89
+ var init_yjsValues = __esm({
90
+ "src/utils/yjsValues.ts"() {
91
+ "use strict";
92
+ Y = __toESM(require("yjs"), 1);
93
+ }
94
+ });
95
+
96
+ // src/models/writeLifecycle.ts
97
+ function registerFunctionDefaultGenerator(name, fn) {
98
+ FUNCTION_DEFAULT_GENERATORS.set(name, fn);
99
+ }
100
+ var Y2, import_ulid, KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS;
101
+ var init_writeLifecycle = __esm({
102
+ "src/models/writeLifecycle.ts"() {
103
+ "use strict";
104
+ Y2 = __toESM(require("yjs"), 1);
105
+ import_ulid = require("ulid");
106
+ KNOWN_FIELD_TYPE_MAP = {
107
+ string: true,
108
+ number: true,
109
+ boolean: true,
110
+ date: true,
111
+ id: true,
112
+ stringset: true
113
+ };
114
+ KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
115
+ FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
116
+ registerFunctionDefaultGenerator("generate_ulid", () => (0, import_ulid.ulid)());
117
+ }
118
+ });
119
+
87
120
  // src/models/metaSync.ts
88
121
  function registerFunctionDefault(fn, name) {
89
122
  KNOWN_FUNCTION_DEFAULTS.set(fn, name);
90
123
  }
91
- var Y, KNOWN_FUNCTION_DEFAULTS;
124
+ var Y3, KNOWN_FUNCTION_DEFAULTS;
92
125
  var init_metaSync = __esm({
93
126
  "src/models/metaSync.ts"() {
94
127
  "use strict";
95
- Y = __toESM(require("yjs"), 1);
128
+ Y3 = __toESM(require("yjs"), 1);
129
+ init_yjsValues();
130
+ init_writeLifecycle();
96
131
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
97
132
  }
98
133
  });
99
134
 
135
+ // src/utils/uniqueIndex.ts
136
+ var Y4;
137
+ var init_uniqueIndex = __esm({
138
+ "src/utils/uniqueIndex.ts"() {
139
+ "use strict";
140
+ Y4 = __toESM(require("yjs"), 1);
141
+ }
142
+ });
143
+
100
144
  // src/models/BaseModel.ts
101
145
  function generateULID() {
102
- return (0, import_ulid.ulid)();
146
+ return (0, import_ulid2.ulid)();
103
147
  }
104
- var Y2, import_ulid, Logger;
148
+ var Y5, import_ulid2, Logger;
105
149
  var init_BaseModel = __esm({
106
150
  "src/models/BaseModel.ts"() {
107
151
  "use strict";
108
- Y2 = __toESM(require("yjs"), 1);
109
- import_ulid = require("ulid");
152
+ Y5 = __toESM(require("yjs"), 1);
153
+ import_ulid2 = require("ulid");
110
154
  init_StringSet();
111
155
  init_documentTypes();
112
156
  init_DocumentQueryTranslator();
113
157
  init_CursorManager();
114
158
  init_sql();
115
159
  init_metaSync();
160
+ init_writeLifecycle();
161
+ init_yjsValues();
162
+ init_uniqueIndex();
116
163
  Logger = class {
117
164
  static _logLevel = 1 /* ERROR */;
118
165
  static _logCallback = null;
package/dist/client.d.cts CHANGED
@@ -31,8 +31,32 @@ interface FieldOptions {
31
31
  unique?: boolean;
32
32
  default?: any | (() => any);
33
33
  autoAssign?: boolean;
34
+ /**
35
+ * Auto-timestamp this field with `Date.now()` (milliseconds) on save.
36
+ *
37
+ * - `'create'` — stamp only on the first save (when `isNew === true`).
38
+ * - `'update'` — stamp on every save (create AND update).
39
+ * - `'both'` — stamp on every save (create AND update).
40
+ *
41
+ * If the caller passes an explicit value for the field in `data`, the
42
+ * explicit value wins and the stamp is skipped. The stamp is applied
43
+ * in `beforeSave` BEFORE any user-defined hooks, so user hooks may
44
+ * still overwrite the value if they wish (last writer wins).
45
+ */
46
+ autoStamp?: "create" | "update" | "both";
34
47
  maxLength?: number;
35
48
  maxCount?: number;
49
+ /**
50
+ * Allowed-value set for a `string` field. When present, the codegen
51
+ * generators (database-type codegen + doc-model v2 codegen) emit a
52
+ * TypeScript string-literal union (`"a" | "b" | "c"`) instead of a bare
53
+ * `string` for this field.
54
+ *
55
+ * Advisory / codegen-only: this is a TS-emission hint. The runtime and the
56
+ * server do NOT enforce enum membership on write (see #843). Only valid on
57
+ * `string` fields, and must be a non-empty array of strings.
58
+ */
59
+ enum?: string[];
36
60
  }
37
61
  interface UniqueConstraintConfig {
38
62
  name: string;
@@ -394,6 +418,17 @@ declare class BaseModel implements StringSetChangeTracker {
394
418
  private ensureLocalChanges;
395
419
  private hasLocalChange;
396
420
  private getFromYjs;
421
+ /**
422
+ * Defensive normalization for the read/sync boundary (issue #625).
423
+ *
424
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
425
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
426
+ * through untouched. For any other field, a composite Yjs primitive
427
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
428
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
429
+ * warn, rather than throwing. Scalars pass through unchanged.
430
+ */
431
+ private normalizeNonStringSetValue;
397
432
  private getValue;
398
433
  private setValue;
399
434
  get isDirty(): boolean;
@@ -528,6 +563,27 @@ declare class BaseModel implements StringSetChangeTracker {
528
563
  */
529
564
  static count(this: new (...args: any[]) => any, filter?: DocumentFilter, options?: Pick<QueryOptions, "documents">): Promise<number>;
530
565
  static findAll<T extends BaseModel>(this: new (...args: any[]) => T): Promise<T[]>;
566
+ /**
567
+ * Resolve a record by its unique-constraint field values via an engine query.
568
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
569
+ * entry for a key.
570
+ *
571
+ * The index map is a cache over the SQLite projection, not the source of
572
+ * truth: every record is loaded into the projection on `initializeForDocument`
573
+ * regardless of index presence, but a record written by a client predating
574
+ * index maintenance (legacy data) is missing from the index. Such a record
575
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
576
+ * check. A query over the constraint fields finds it reliably.
577
+ *
578
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
579
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
580
+ * whether to heal the missing index entry (write paths) and do their own
581
+ * logging — this method never mutates the index, so read paths stay
582
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
583
+ * constraint field means the record does not participate in the constraint,
584
+ * so no query runs.
585
+ */
586
+ private static _resolveByUniqueFallback;
531
587
  static findByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, value: any | any[]): Promise<T | null>;
532
588
  static upsertByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, uniqueLookupValue: any | any[], dataToUpsert: Partial<Omit<InstanceType<new (...args: any[]) => T>, keyof BaseModel | "toJSON">> & {
533
589
  id?: string;
@@ -682,6 +738,25 @@ interface SaveRequest {
682
738
  ifNotExists?: boolean;
683
739
  condition?: DocumentFilter;
684
740
  upsertOn?: string;
741
+ /**
742
+ * Per-call auto-timestamp directives for schemaless writes.
743
+ *
744
+ * Each entry is a list of field names to stamp with `Date.now()`
745
+ * (milliseconds) at the corresponding lifecycle event:
746
+ *
747
+ * - `create` — field is stamped only when the record is new
748
+ * (no row exists yet for this `modelName` + `id`).
749
+ * - `update` — field is stamped on every save (create AND update).
750
+ *
751
+ * If the caller already provided an explicit value for the field
752
+ * in `data`, the explicit value wins and the stamp is skipped.
753
+ * Stamping happens BEFORE the `beforeSave` user hook runs, so user
754
+ * hooks may still overwrite the stamped value if they wish.
755
+ */
756
+ autoStamps?: {
757
+ create?: string[];
758
+ update?: string[];
759
+ };
685
760
  }
686
761
  interface DeleteRequest {
687
762
  modelName: string;
@@ -704,6 +779,20 @@ interface QueryResponse {
704
779
  interface SaveResponse {
705
780
  success: boolean;
706
781
  id: string;
782
+ /**
783
+ * Server-applied field values the DO wrote AFTER the caller's submission
784
+ * inputs were assembled, read back from the final post-hook `data`
785
+ * (#1476, consolidating the #1462 `stamps` channel). This is the single
786
+ * "server-applied fields" channel: it carries both the applied `autoStamps`
787
+ * timestamps and any fields a `beforeSave` hook reported via
788
+ * `serverAppliedFieldNames` (e.g. app-level trigger-computed values). Present
789
+ * only when at least one such field ended up with a value. Lets the Worker
790
+ * thread these values into the change-event `data` it broadcasts to
791
+ * subscribers — otherwise a `select`-listed stamped or trigger-computed field
792
+ * never reaches the frame, because the frame is rebuilt from the submission
793
+ * inputs (which predate the DO-side write).
794
+ */
795
+ appliedFields?: Record<string, any>;
707
796
  }
708
797
  interface DeleteResponse {
709
798
  success: boolean;
@@ -745,10 +834,23 @@ interface PatchRequest {
745
834
  data: Record<string, any>;
746
835
  stringSets?: Record<string, string[]>;
747
836
  condition?: DocumentFilter;
837
+ /**
838
+ * Per-call auto-timestamp directives. A patch is always treated as
839
+ * an update (`isNew === false`), so only the `update` list is
840
+ * applied; the `create` list is ignored on patch.
841
+ *
842
+ * Explicit values in `data` win.
843
+ */
844
+ autoStamps?: {
845
+ create?: string[];
846
+ update?: string[];
847
+ };
748
848
  }
749
849
  interface PatchResponse {
750
850
  success: boolean;
751
851
  id: string;
852
+ /** Server-applied field values (#1476). See `SaveResponse.appliedFields`. */
853
+ appliedFields?: Record<string, any>;
752
854
  }
753
855
  interface BatchOperation {
754
856
  op: "save" | "patch" | "delete" | "increment" | "addToSet" | "removeFromSet";
@@ -760,15 +862,62 @@ interface BatchOperation {
760
862
  ifNotExists?: boolean;
761
863
  condition?: DocumentFilter;
762
864
  upsertOn?: string;
865
+ /**
866
+ * Per-operation auto-timestamp directives. Same semantics as
867
+ * `SaveRequest.autoStamps` / `PatchRequest.autoStamps`. Applied
868
+ * BEFORE the user `beforeSave` hook runs, with explicit values
869
+ * in `data` always winning.
870
+ *
871
+ * For `op: "patch"`, only the `update` list applies (patches are
872
+ * always treated as updates).
873
+ */
874
+ autoStamps?: {
875
+ create?: string[];
876
+ update?: string[];
877
+ };
763
878
  }
764
879
  interface BatchRequest {
765
880
  operations: BatchOperation[];
881
+ /**
882
+ * All-or-nothing execution (#1478). When `true`, the FIRST operation that
883
+ * fails inside the transaction — hook denial, condition / `ifNotExists` /
884
+ * unique violation, record-not-found, or upsert validation error — aborts
885
+ * the whole batch: every write is rolled back and the handler returns an
886
+ * error naming the failed step with its natural HTTP status (see
887
+ * `BatchOperationResult.status`). When `false` or absent (the default), each
888
+ * operation's outcome is recorded independently and successful siblings
889
+ * commit — the partial-success behavior import-bulk and applyToQuery rely on.
890
+ */
891
+ atomic?: boolean;
766
892
  }
893
+ /**
894
+ * Stable, machine-readable failure tag on a failed `BatchOperationResult`
895
+ * (#1478). Derived at each failure site — never inferred from the `error`
896
+ * string. `#1389` (conditional writes + fencing-token enforcement) surfaces its
897
+ * condition / stale-token rejections through this same channel.
898
+ */
899
+ type BatchFailureCode = "NOT_FOUND" | "HOOK_DENIED" | "CONDITION_NOT_MET" | "ALREADY_EXISTS" | "UNIQUE_VIOLATION" | "VALIDATION_ERROR";
767
900
  interface BatchOperationResult {
768
901
  success: boolean;
769
902
  id: string;
770
903
  error?: string;
904
+ /**
905
+ * Structured failure metadata (#1478), present only on a failed result. Both
906
+ * are derived at the failure site, not parsed back out of `error`:
907
+ * - `status` — the natural HTTP status for the cause (404 not found,
908
+ * 403 hook denial, 409 condition / `ifNotExists` / unique conflict,
909
+ * 400 upsert validation).
910
+ * - `code` — a stable machine-readable tag (see `BatchFailureCode`).
911
+ *
912
+ * In atomic mode the handler reads the first failure's `status`/`code` to
913
+ * build the error response, so an increment-on-missing op keeps its 404 and a
914
+ * hook denial keeps its 403 instead of collapsing to a uniform 409.
915
+ */
916
+ status?: number;
917
+ code?: BatchFailureCode;
771
918
  values?: Record<string, number>;
919
+ /** Server-applied field values (#1476). See `SaveResponse.appliedFields`. */
920
+ appliedFields?: Record<string, any>;
772
921
  }
773
922
  interface BatchResponse {
774
923
  results: BatchOperationResult[];
package/dist/client.d.ts CHANGED
@@ -31,8 +31,32 @@ interface FieldOptions {
31
31
  unique?: boolean;
32
32
  default?: any | (() => any);
33
33
  autoAssign?: boolean;
34
+ /**
35
+ * Auto-timestamp this field with `Date.now()` (milliseconds) on save.
36
+ *
37
+ * - `'create'` — stamp only on the first save (when `isNew === true`).
38
+ * - `'update'` — stamp on every save (create AND update).
39
+ * - `'both'` — stamp on every save (create AND update).
40
+ *
41
+ * If the caller passes an explicit value for the field in `data`, the
42
+ * explicit value wins and the stamp is skipped. The stamp is applied
43
+ * in `beforeSave` BEFORE any user-defined hooks, so user hooks may
44
+ * still overwrite the value if they wish (last writer wins).
45
+ */
46
+ autoStamp?: "create" | "update" | "both";
34
47
  maxLength?: number;
35
48
  maxCount?: number;
49
+ /**
50
+ * Allowed-value set for a `string` field. When present, the codegen
51
+ * generators (database-type codegen + doc-model v2 codegen) emit a
52
+ * TypeScript string-literal union (`"a" | "b" | "c"`) instead of a bare
53
+ * `string` for this field.
54
+ *
55
+ * Advisory / codegen-only: this is a TS-emission hint. The runtime and the
56
+ * server do NOT enforce enum membership on write (see #843). Only valid on
57
+ * `string` fields, and must be a non-empty array of strings.
58
+ */
59
+ enum?: string[];
36
60
  }
37
61
  interface UniqueConstraintConfig {
38
62
  name: string;
@@ -394,6 +418,17 @@ declare class BaseModel implements StringSetChangeTracker {
394
418
  private ensureLocalChanges;
395
419
  private hasLocalChange;
396
420
  private getFromYjs;
421
+ /**
422
+ * Defensive normalization for the read/sync boundary (issue #625).
423
+ *
424
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
425
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
426
+ * through untouched. For any other field, a composite Yjs primitive
427
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
428
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
429
+ * warn, rather than throwing. Scalars pass through unchanged.
430
+ */
431
+ private normalizeNonStringSetValue;
397
432
  private getValue;
398
433
  private setValue;
399
434
  get isDirty(): boolean;
@@ -528,6 +563,27 @@ declare class BaseModel implements StringSetChangeTracker {
528
563
  */
529
564
  static count(this: new (...args: any[]) => any, filter?: DocumentFilter, options?: Pick<QueryOptions, "documents">): Promise<number>;
530
565
  static findAll<T extends BaseModel>(this: new (...args: any[]) => T): Promise<T[]>;
566
+ /**
567
+ * Resolve a record by its unique-constraint field values via an engine query.
568
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
569
+ * entry for a key.
570
+ *
571
+ * The index map is a cache over the SQLite projection, not the source of
572
+ * truth: every record is loaded into the projection on `initializeForDocument`
573
+ * regardless of index presence, but a record written by a client predating
574
+ * index maintenance (legacy data) is missing from the index. Such a record
575
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
576
+ * check. A query over the constraint fields finds it reliably.
577
+ *
578
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
579
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
580
+ * whether to heal the missing index entry (write paths) and do their own
581
+ * logging — this method never mutates the index, so read paths stay
582
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
583
+ * constraint field means the record does not participate in the constraint,
584
+ * so no query runs.
585
+ */
586
+ private static _resolveByUniqueFallback;
531
587
  static findByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, value: any | any[]): Promise<T | null>;
532
588
  static upsertByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, uniqueLookupValue: any | any[], dataToUpsert: Partial<Omit<InstanceType<new (...args: any[]) => T>, keyof BaseModel | "toJSON">> & {
533
589
  id?: string;
@@ -682,6 +738,25 @@ interface SaveRequest {
682
738
  ifNotExists?: boolean;
683
739
  condition?: DocumentFilter;
684
740
  upsertOn?: string;
741
+ /**
742
+ * Per-call auto-timestamp directives for schemaless writes.
743
+ *
744
+ * Each entry is a list of field names to stamp with `Date.now()`
745
+ * (milliseconds) at the corresponding lifecycle event:
746
+ *
747
+ * - `create` — field is stamped only when the record is new
748
+ * (no row exists yet for this `modelName` + `id`).
749
+ * - `update` — field is stamped on every save (create AND update).
750
+ *
751
+ * If the caller already provided an explicit value for the field
752
+ * in `data`, the explicit value wins and the stamp is skipped.
753
+ * Stamping happens BEFORE the `beforeSave` user hook runs, so user
754
+ * hooks may still overwrite the stamped value if they wish.
755
+ */
756
+ autoStamps?: {
757
+ create?: string[];
758
+ update?: string[];
759
+ };
685
760
  }
686
761
  interface DeleteRequest {
687
762
  modelName: string;
@@ -704,6 +779,20 @@ interface QueryResponse {
704
779
  interface SaveResponse {
705
780
  success: boolean;
706
781
  id: string;
782
+ /**
783
+ * Server-applied field values the DO wrote AFTER the caller's submission
784
+ * inputs were assembled, read back from the final post-hook `data`
785
+ * (#1476, consolidating the #1462 `stamps` channel). This is the single
786
+ * "server-applied fields" channel: it carries both the applied `autoStamps`
787
+ * timestamps and any fields a `beforeSave` hook reported via
788
+ * `serverAppliedFieldNames` (e.g. app-level trigger-computed values). Present
789
+ * only when at least one such field ended up with a value. Lets the Worker
790
+ * thread these values into the change-event `data` it broadcasts to
791
+ * subscribers — otherwise a `select`-listed stamped or trigger-computed field
792
+ * never reaches the frame, because the frame is rebuilt from the submission
793
+ * inputs (which predate the DO-side write).
794
+ */
795
+ appliedFields?: Record<string, any>;
707
796
  }
708
797
  interface DeleteResponse {
709
798
  success: boolean;
@@ -745,10 +834,23 @@ interface PatchRequest {
745
834
  data: Record<string, any>;
746
835
  stringSets?: Record<string, string[]>;
747
836
  condition?: DocumentFilter;
837
+ /**
838
+ * Per-call auto-timestamp directives. A patch is always treated as
839
+ * an update (`isNew === false`), so only the `update` list is
840
+ * applied; the `create` list is ignored on patch.
841
+ *
842
+ * Explicit values in `data` win.
843
+ */
844
+ autoStamps?: {
845
+ create?: string[];
846
+ update?: string[];
847
+ };
748
848
  }
749
849
  interface PatchResponse {
750
850
  success: boolean;
751
851
  id: string;
852
+ /** Server-applied field values (#1476). See `SaveResponse.appliedFields`. */
853
+ appliedFields?: Record<string, any>;
752
854
  }
753
855
  interface BatchOperation {
754
856
  op: "save" | "patch" | "delete" | "increment" | "addToSet" | "removeFromSet";
@@ -760,15 +862,62 @@ interface BatchOperation {
760
862
  ifNotExists?: boolean;
761
863
  condition?: DocumentFilter;
762
864
  upsertOn?: string;
865
+ /**
866
+ * Per-operation auto-timestamp directives. Same semantics as
867
+ * `SaveRequest.autoStamps` / `PatchRequest.autoStamps`. Applied
868
+ * BEFORE the user `beforeSave` hook runs, with explicit values
869
+ * in `data` always winning.
870
+ *
871
+ * For `op: "patch"`, only the `update` list applies (patches are
872
+ * always treated as updates).
873
+ */
874
+ autoStamps?: {
875
+ create?: string[];
876
+ update?: string[];
877
+ };
763
878
  }
764
879
  interface BatchRequest {
765
880
  operations: BatchOperation[];
881
+ /**
882
+ * All-or-nothing execution (#1478). When `true`, the FIRST operation that
883
+ * fails inside the transaction — hook denial, condition / `ifNotExists` /
884
+ * unique violation, record-not-found, or upsert validation error — aborts
885
+ * the whole batch: every write is rolled back and the handler returns an
886
+ * error naming the failed step with its natural HTTP status (see
887
+ * `BatchOperationResult.status`). When `false` or absent (the default), each
888
+ * operation's outcome is recorded independently and successful siblings
889
+ * commit — the partial-success behavior import-bulk and applyToQuery rely on.
890
+ */
891
+ atomic?: boolean;
766
892
  }
893
+ /**
894
+ * Stable, machine-readable failure tag on a failed `BatchOperationResult`
895
+ * (#1478). Derived at each failure site — never inferred from the `error`
896
+ * string. `#1389` (conditional writes + fencing-token enforcement) surfaces its
897
+ * condition / stale-token rejections through this same channel.
898
+ */
899
+ type BatchFailureCode = "NOT_FOUND" | "HOOK_DENIED" | "CONDITION_NOT_MET" | "ALREADY_EXISTS" | "UNIQUE_VIOLATION" | "VALIDATION_ERROR";
767
900
  interface BatchOperationResult {
768
901
  success: boolean;
769
902
  id: string;
770
903
  error?: string;
904
+ /**
905
+ * Structured failure metadata (#1478), present only on a failed result. Both
906
+ * are derived at the failure site, not parsed back out of `error`:
907
+ * - `status` — the natural HTTP status for the cause (404 not found,
908
+ * 403 hook denial, 409 condition / `ifNotExists` / unique conflict,
909
+ * 400 upsert validation).
910
+ * - `code` — a stable machine-readable tag (see `BatchFailureCode`).
911
+ *
912
+ * In atomic mode the handler reads the first failure's `status`/`code` to
913
+ * build the error response, so an increment-on-missing op keeps its 404 and a
914
+ * hook denial keeps its 403 instead of collapsing to a uniform 409.
915
+ */
916
+ status?: number;
917
+ code?: BatchFailureCode;
771
918
  values?: Record<string, number>;
919
+ /** Server-applied field values (#1476). See `SaveResponse.appliedFields`. */
920
+ appliedFields?: Record<string, any>;
772
921
  }
773
922
  interface BatchResponse {
774
923
  results: BatchOperationResult[];
package/dist/client.js CHANGED
@@ -57,8 +57,40 @@ var init_DocumentQueryTranslator = __esm({
57
57
  }
58
58
  });
59
59
 
60
- // src/models/metaSync.ts
60
+ // src/utils/yjsValues.ts
61
61
  import * as Y from "yjs";
62
+ var init_yjsValues = __esm({
63
+ "src/utils/yjsValues.ts"() {
64
+ "use strict";
65
+ }
66
+ });
67
+
68
+ // src/models/writeLifecycle.ts
69
+ import * as Y2 from "yjs";
70
+ import { ulid } from "ulid";
71
+ function registerFunctionDefaultGenerator(name, fn) {
72
+ FUNCTION_DEFAULT_GENERATORS.set(name, fn);
73
+ }
74
+ var KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS;
75
+ var init_writeLifecycle = __esm({
76
+ "src/models/writeLifecycle.ts"() {
77
+ "use strict";
78
+ KNOWN_FIELD_TYPE_MAP = {
79
+ string: true,
80
+ number: true,
81
+ boolean: true,
82
+ date: true,
83
+ id: true,
84
+ stringset: true
85
+ };
86
+ KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
87
+ FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
88
+ registerFunctionDefaultGenerator("generate_ulid", () => ulid());
89
+ }
90
+ });
91
+
92
+ // src/models/metaSync.ts
93
+ import * as Y3 from "yjs";
62
94
  function registerFunctionDefault(fn, name) {
63
95
  KNOWN_FUNCTION_DEFAULTS.set(fn, name);
64
96
  }
@@ -66,15 +98,25 @@ var KNOWN_FUNCTION_DEFAULTS;
66
98
  var init_metaSync = __esm({
67
99
  "src/models/metaSync.ts"() {
68
100
  "use strict";
101
+ init_yjsValues();
102
+ init_writeLifecycle();
69
103
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
70
104
  }
71
105
  });
72
106
 
107
+ // src/utils/uniqueIndex.ts
108
+ import * as Y4 from "yjs";
109
+ var init_uniqueIndex = __esm({
110
+ "src/utils/uniqueIndex.ts"() {
111
+ "use strict";
112
+ }
113
+ });
114
+
73
115
  // src/models/BaseModel.ts
74
- import * as Y2 from "yjs";
75
- import { ulid } from "ulid";
116
+ import * as Y5 from "yjs";
117
+ import { ulid as ulid2 } from "ulid";
76
118
  function generateULID() {
77
- return ulid();
119
+ return ulid2();
78
120
  }
79
121
  var Logger;
80
122
  var init_BaseModel = __esm({
@@ -86,6 +128,9 @@ var init_BaseModel = __esm({
86
128
  init_CursorManager();
87
129
  init_sql();
88
130
  init_metaSync();
131
+ init_writeLifecycle();
132
+ init_yjsValues();
133
+ init_uniqueIndex();
89
134
  Logger = class {
90
135
  static _logLevel = 1 /* ERROR */;
91
136
  static _logCallback = null;