js-bao 0.4.0 → 0.4.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/browser.cjs CHANGED
@@ -2572,6 +2572,9 @@ var init_BaseModel = __esm({
2572
2572
  static connectedDocuments = /* @__PURE__ */ new Map();
2573
2573
  static documentYMaps = /* @__PURE__ */ new Map();
2574
2574
  // Maps docId to YMap for that document
2575
+ // Tracks nested-Y.Map stringset fields we've already attached observers to,
2576
+ // so attachStringSetObserversToRecord() is idempotent across re-entry.
2577
+ static _observedStringSetMaps = /* @__PURE__ */ new WeakSet();
2575
2578
  // Copy-on-write state management
2576
2579
  _localChanges = null;
2577
2580
  _isDirty = false;
@@ -3091,16 +3094,15 @@ var init_BaseModel = __esm({
3091
3094
  this._isDirty = true;
3092
3095
  }
3093
3096
  getStringSetCurrentValues(fieldName) {
3094
- const yjsData = this.getFromYjs(fieldName);
3095
- if (yjsData && typeof yjsData === "object") {
3096
- return Object.keys(yjsData);
3097
- }
3098
- return [];
3097
+ return this.getStringSetFromYjs(fieldName);
3099
3098
  }
3100
3099
  getStringSetFromYjs(fieldName) {
3101
3100
  const yjsData = this.getFromYjs(fieldName);
3101
+ if (yjsData instanceof Y2.Map) {
3102
+ return Array.from(yjsData.keys());
3103
+ }
3102
3104
  if (yjsData && typeof yjsData === "object") {
3103
- return Object.keys(yjsData);
3105
+ return Object.keys(yjsData).filter((k) => Boolean(yjsData[k]));
3104
3106
  }
3105
3107
  return [];
3106
3108
  }
@@ -3711,7 +3713,7 @@ var init_BaseModel = __esm({
3711
3713
  Logger.debug(
3712
3714
  `[_diffWithYjsData] No unsaved changes, returning empty diff`
3713
3715
  );
3714
- return { added: {}, modified: {}, removed: [] };
3716
+ return { added: {}, modified: {}, removed: [], stringSetChanges: {} };
3715
3717
  }
3716
3718
  const modelConstructor = this.constructor;
3717
3719
  const schema = modelConstructor.getSchema();
@@ -3742,6 +3744,7 @@ var init_BaseModel = __esm({
3742
3744
  const added = {};
3743
3745
  const modified = {};
3744
3746
  const removed = [];
3747
+ const stringSetChanges = {};
3745
3748
  if (!recordYMap) {
3746
3749
  Logger.debug(
3747
3750
  `[_diffWithYjsData] No existing recordYMap, treating all local changes as 'added'`
@@ -3750,11 +3753,12 @@ var init_BaseModel = __esm({
3750
3753
  for (const [key, value] of Object.entries(this._localChanges)) {
3751
3754
  Logger.debug(`[_diffWithYjsData] Adding field '${key}': ${value}`);
3752
3755
  if (value && value.type === "stringset") {
3753
- const stringSetData = {};
3754
- for (const addition of value.additions) {
3755
- stringSetData[addition] = true;
3756
+ if (value.additions.size > 0 || value.removals.size > 0) {
3757
+ stringSetChanges[key] = {
3758
+ additions: value.additions,
3759
+ removals: value.removals
3760
+ };
3756
3761
  }
3757
- added[key] = stringSetData;
3758
3762
  } else {
3759
3763
  added[key] = value;
3760
3764
  }
@@ -3763,9 +3767,10 @@ var init_BaseModel = __esm({
3763
3767
  Logger.debug(`[_diffWithYjsData] Final diff for new record:`, {
3764
3768
  added,
3765
3769
  modified: {},
3766
- removed: []
3770
+ removed: [],
3771
+ stringSetChanges
3767
3772
  });
3768
- return { added, modified, removed: [] };
3773
+ return { added, modified, removed: [], stringSetChanges };
3769
3774
  }
3770
3775
  Logger.debug(
3771
3776
  `[_diffWithYjsData] Existing record found, comparing local changes with Y.js data`
@@ -3782,21 +3787,11 @@ var init_BaseModel = __esm({
3782
3787
  `[_diffWithYjsData] Processing field '${key}' with local value: ${localValue}`
3783
3788
  );
3784
3789
  if (localValue && localValue.type === "stringset") {
3785
- const currentYjsData = recordYMap.get(key) || {};
3786
- const newStringSetData = {
3787
- ...currentYjsData
3788
- };
3789
- for (const addition of localValue.additions) {
3790
- newStringSetData[addition] = true;
3791
- }
3792
- for (const removal of localValue.removals) {
3793
- delete newStringSetData[removal];
3794
- }
3795
- const yjsValue = recordYMap.get(key);
3796
- if (yjsValue === void 0) {
3797
- added[key] = newStringSetData;
3798
- } else if (!this._deepEqual(yjsValue, newStringSetData)) {
3799
- modified[key] = newStringSetData;
3790
+ if (localValue.additions.size > 0 || localValue.removals.size > 0) {
3791
+ stringSetChanges[key] = {
3792
+ additions: localValue.additions,
3793
+ removals: localValue.removals
3794
+ };
3800
3795
  }
3801
3796
  } else {
3802
3797
  const yjsValue = recordYMap.get(key);
@@ -3822,14 +3817,43 @@ var init_BaseModel = __esm({
3822
3817
  Logger.debug(`[_diffWithYjsData] Final diff result:`, {
3823
3818
  added,
3824
3819
  modified,
3825
- removed
3820
+ removed,
3821
+ stringSetChanges
3826
3822
  });
3827
3823
  Logger.verbose(`[${modelConstructor.name}] Diff for ${this.id}:`, {
3828
3824
  added: Object.keys(added),
3829
3825
  modified: Object.keys(modified),
3830
- removed
3826
+ removed,
3827
+ stringSetChanges: Object.keys(stringSetChanges)
3831
3828
  });
3832
- return { added, modified, removed };
3829
+ return { added, modified, removed, stringSetChanges };
3830
+ }
3831
+ /**
3832
+ * Apply a stringset change set to the parent record's Y.Map by mutating a
3833
+ * nested Y.Map keyed by member. Migrates from a legacy plain-object value
3834
+ * if the field hasn't been touched since the wire-format change in #561.
3835
+ */
3836
+ applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3837
+ let nested = recordYMap.get(fieldName);
3838
+ if (!(nested instanceof Y2.Map)) {
3839
+ const migrated = new Y2.Map();
3840
+ if (nested && typeof nested === "object") {
3841
+ for (const [member, marker] of Object.entries(
3842
+ nested
3843
+ )) {
3844
+ if (Boolean(marker)) migrated.set(member, true);
3845
+ }
3846
+ }
3847
+ recordYMap.set(fieldName, migrated);
3848
+ nested = migrated;
3849
+ }
3850
+ const target = nested;
3851
+ for (const member of change.additions) {
3852
+ target.set(member, true);
3853
+ }
3854
+ for (const member of change.removals) {
3855
+ target.delete(member);
3856
+ }
3833
3857
  }
3834
3858
  /**
3835
3859
  * Deep equality check for comparing field values
@@ -4068,7 +4092,7 @@ var init_BaseModel = __esm({
4068
4092
  Logger.debug(`[${modelName}.save] About to calculate diff`);
4069
4093
  const diff = this._diffWithYjsData();
4070
4094
  Logger.debug(`[${modelName}.save] Diff result:`, diff);
4071
- const hasChanges = Object.keys(diff.added).length > 0 || Object.keys(diff.modified).length > 0 || diff.removed.length > 0;
4095
+ const hasChanges = Object.keys(diff.added).length > 0 || Object.keys(diff.modified).length > 0 || diff.removed.length > 0 || Object.keys(diff.stringSetChanges).length > 0;
4072
4096
  Logger.debug(`[${modelName}.save] hasChanges: ${hasChanges}`);
4073
4097
  if (!hasChanges && isUpdate) {
4074
4098
  Logger.verbose(
@@ -4186,6 +4210,16 @@ var init_BaseModel = __esm({
4186
4210
  Logger.debug(`[${modelName}.save] Removing field '${key}'`);
4187
4211
  recordYMap.delete(key);
4188
4212
  }
4213
+ for (const [fieldName, change] of Object.entries(diff.stringSetChanges)) {
4214
+ Logger.debug(
4215
+ `[${modelName}.save] Applying stringset change to field '${fieldName}':`,
4216
+ {
4217
+ additions: Array.from(change.additions),
4218
+ removals: Array.from(change.removals)
4219
+ }
4220
+ );
4221
+ this.applyStringSetChangeToYMap(recordYMap, fieldName, change);
4222
+ }
4189
4223
  Logger.debug(
4190
4224
  `[${modelName}.save] After applying changes, recordYMap contents:`,
4191
4225
  Object.fromEntries(recordYMap.entries())
@@ -5370,6 +5404,39 @@ var init_BaseModel = __esm({
5370
5404
  _BaseModel.prototype.setValue = originalSetValue;
5371
5405
  }
5372
5406
  }
5407
+ /**
5408
+ * Attach a one-shot observer to a nested-Y.Map stringset field. Calls
5409
+ * notifyListeners() when the nested map's keys change (i.e. when a remote
5410
+ * member arrives via Y.applyUpdate, or a local per-member set/delete).
5411
+ * Idempotent — re-calling on the same Y.Map is a no-op.
5412
+ */
5413
+ static observeStringSetMapOnce(nestedMap) {
5414
+ if (_BaseModel._observedStringSetMaps.has(nestedMap)) return;
5415
+ _BaseModel._observedStringSetMaps.add(nestedMap);
5416
+ const modelConstructor = this;
5417
+ nestedMap.observe(() => {
5418
+ Logger.verbose(
5419
+ `[${modelConstructor.name}] Nested stringset Y.Map changed; notifying listeners`
5420
+ );
5421
+ modelConstructor.notifyListeners();
5422
+ });
5423
+ }
5424
+ /**
5425
+ * Walk the schema's stringset fields on `recordYMap` and observe any nested
5426
+ * Y.Map values. Called from observer setup and from the parent observer body
5427
+ * so newly-arrived stringset Y.Maps (local migration or remote create) get
5428
+ * an observer too.
5429
+ */
5430
+ static attachStringSetObserversToRecord(recordYMap, schema) {
5431
+ if (!schema?.fields) return;
5432
+ for (const [fieldKey, fieldOptions] of schema.fields) {
5433
+ if (fieldOptions?.type !== "stringset") continue;
5434
+ const value = recordYMap.get(fieldKey);
5435
+ if (value instanceof Y2.Map) {
5436
+ this.observeStringSetMapOnce(value);
5437
+ }
5438
+ }
5439
+ }
5373
5440
  /**
5374
5441
  * Sets up deep observation on a nested YMap to sync field-level changes to the database
5375
5442
  */
@@ -5386,11 +5453,13 @@ var init_BaseModel = __esm({
5386
5453
  Logger.verbose(
5387
5454
  `[${modelName}] Setting up nested YMap observer for record ${recordId}`
5388
5455
  );
5456
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
5389
5457
  recordYMap.observe(async (event) => {
5390
5458
  Logger.verbose(
5391
5459
  `[${modelName}] Nested YMap change detected for record ${recordId}:`,
5392
5460
  event
5393
5461
  );
5462
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
5394
5463
  const currentDbInstance = _BaseModel.dbInstance;
5395
5464
  if (!currentDbInstance) {
5396
5465
  Logger.error(
@@ -5464,11 +5533,13 @@ var init_BaseModel = __esm({
5464
5533
  Logger.verbose(
5465
5534
  `[${modelName}] Setting up nested YMap observer for record ${recordId} in document ${docId}`
5466
5535
  );
5536
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
5467
5537
  recordYMap.observe(async (event) => {
5468
5538
  Logger.verbose(
5469
5539
  `[${modelName}] Nested YMap change detected for record ${recordId} in document ${docId}:`,
5470
5540
  event
5471
5541
  );
5542
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
5472
5543
  const currentDbInstance = _BaseModel.dbInstance;
5473
5544
  if (!currentDbInstance) {
5474
5545
  Logger.error(
@@ -6343,6 +6414,7 @@ function defineModelSchema(input) {
6343
6414
  const { name, fields } = input;
6344
6415
  const options = {
6345
6416
  name,
6417
+ className: input.options?.className,
6346
6418
  uniqueConstraints: input.options?.uniqueConstraints ? [...input.options.uniqueConstraints] : void 0,
6347
6419
  relationships: input.options?.relationships
6348
6420
  };
@@ -6442,18 +6514,13 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
6442
6514
  function attachSchemaToClass(modelClass, schema) {
6443
6515
  modelClass.schema = schema;
6444
6516
  modelClass.modelName = schema.options.name;
6445
- if (!modelClass.getSchema) {
6446
- Object.defineProperty(modelClass, "getSchema", {
6447
- value: function() {
6448
- return schema.buildRuntimeShape(modelClass);
6449
- },
6450
- writable: false
6451
- });
6452
- } else {
6453
- modelClass.getSchema = function() {
6517
+ Object.defineProperty(modelClass, "getSchema", {
6518
+ value: function() {
6454
6519
  return schema.buildRuntimeShape(modelClass);
6455
- };
6456
- }
6520
+ },
6521
+ writable: false,
6522
+ configurable: true
6523
+ });
6457
6524
  const runtimeShape = schema.buildRuntimeShape(modelClass);
6458
6525
  BaseModel2.attachFieldAccessors(modelClass, runtimeShape.fields);
6459
6526
  return runtimeShape;
@@ -6686,12 +6753,52 @@ var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
6686
6753
  "id",
6687
6754
  "stringset"
6688
6755
  ]);
6689
- function parseFieldOptions(raw) {
6756
+ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
6757
+ "type",
6758
+ "indexed",
6759
+ "unique",
6760
+ "required",
6761
+ "auto_assign",
6762
+ "max_length",
6763
+ "max_count",
6764
+ "default"
6765
+ ]);
6766
+ var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
6767
+ "fields",
6768
+ "relationships",
6769
+ "unique_constraints",
6770
+ "class_name"
6771
+ ]);
6772
+ var KNOWN_RELATIONSHIP_KEYS = /* @__PURE__ */ new Set([
6773
+ "type",
6774
+ "model",
6775
+ "related_id_field",
6776
+ "join_model",
6777
+ "join_model_local_field",
6778
+ "join_model_related_field",
6779
+ "order_by_field",
6780
+ "order_direction",
6781
+ "join_model_order_by_field",
6782
+ "join_model_order_direction"
6783
+ ]);
6784
+ var KNOWN_UNIQUE_CONSTRAINT_KEYS = /* @__PURE__ */ new Set(["name", "fields"]);
6785
+ function checkUnknownKeys(raw, known, context, strict) {
6786
+ if (!strict) return;
6787
+ for (const key of Object.keys(raw)) {
6788
+ if (!known.has(key)) {
6789
+ throw new Error(
6790
+ `${context}: unknown key "${key}". Allowed: ${[...known].join(", ")}`
6791
+ );
6792
+ }
6793
+ }
6794
+ }
6795
+ function parseFieldOptions(raw, context, strict) {
6690
6796
  if (!raw.type || !VALID_FIELD_TYPES.has(raw.type)) {
6691
6797
  throw new Error(
6692
6798
  `Invalid field type "${raw.type}". Must be one of: ${[...VALID_FIELD_TYPES].join(", ")}`
6693
6799
  );
6694
6800
  }
6801
+ checkUnknownKeys(raw, KNOWN_FIELD_KEYS, context, strict);
6695
6802
  const opts = { type: raw.type };
6696
6803
  if (raw.indexed === true) opts.indexed = true;
6697
6804
  if (raw.unique === true) opts.unique = true;
@@ -6707,8 +6814,9 @@ function requireField(raw, field, context) {
6707
6814
  throw new Error(`Relationship ${context}: missing required field "${field}"`);
6708
6815
  }
6709
6816
  }
6710
- function parseRelationship(raw) {
6817
+ function parseRelationship(raw, context, strict) {
6711
6818
  const type = raw.type;
6819
+ checkUnknownKeys(raw, KNOWN_RELATIONSHIP_KEYS, context, strict);
6712
6820
  if (type === "refersTo") {
6713
6821
  requireField(raw, "model", "refersTo");
6714
6822
  requireField(raw, "related_id_field", "refersTo");
@@ -6750,7 +6858,8 @@ function parseRelationship(raw) {
6750
6858
  }
6751
6859
  throw new Error(`Unknown relationship type: ${type}`);
6752
6860
  }
6753
- function loadSchemaFromTomlString(tomlString) {
6861
+ function loadSchemaFromTomlString(tomlString, options = {}) {
6862
+ const strict = options.strict !== false;
6754
6863
  const parsed = (0, import_smol_toml.parse)(tomlString);
6755
6864
  const models = parsed.models;
6756
6865
  if (!models || typeof models !== "object") {
@@ -6758,10 +6867,20 @@ function loadSchemaFromTomlString(tomlString) {
6758
6867
  }
6759
6868
  const schemas = [];
6760
6869
  for (const [modelName, modelDef] of Object.entries(models)) {
6870
+ checkUnknownKeys(
6871
+ modelDef,
6872
+ KNOWN_MODEL_KEYS,
6873
+ `[models.${modelName}]`,
6874
+ strict
6875
+ );
6761
6876
  const fields = {};
6762
6877
  if (modelDef.fields) {
6763
6878
  for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
6764
- fields[fieldName] = parseFieldOptions(fieldDef);
6879
+ fields[fieldName] = parseFieldOptions(
6880
+ fieldDef,
6881
+ `[models.${modelName}.fields.${fieldName}]`,
6882
+ strict
6883
+ );
6765
6884
  }
6766
6885
  }
6767
6886
  let relationships;
@@ -6770,24 +6889,36 @@ function loadSchemaFromTomlString(tomlString) {
6770
6889
  for (const [relName, relDef] of Object.entries(
6771
6890
  modelDef.relationships
6772
6891
  )) {
6773
- relationships[relName] = parseRelationship(relDef);
6892
+ relationships[relName] = parseRelationship(
6893
+ relDef,
6894
+ `[models.${modelName}.relationships.${relName}]`,
6895
+ strict
6896
+ );
6774
6897
  }
6775
6898
  }
6776
6899
  let uniqueConstraints;
6777
6900
  if (modelDef.unique_constraints) {
6778
6901
  uniqueConstraints = [];
6779
6902
  for (const raw of modelDef.unique_constraints) {
6903
+ checkUnknownKeys(
6904
+ raw,
6905
+ KNOWN_UNIQUE_CONSTRAINT_KEYS,
6906
+ `[[models.${modelName}.unique_constraints]]`,
6907
+ strict
6908
+ );
6780
6909
  uniqueConstraints.push({
6781
6910
  name: raw.name,
6782
6911
  fields: [...raw.fields]
6783
6912
  });
6784
6913
  }
6785
6914
  }
6915
+ const className = typeof modelDef.class_name === "string" ? modelDef.class_name : void 0;
6786
6916
  schemas.push(
6787
6917
  defineModelSchema({
6788
6918
  name: modelName,
6789
6919
  fields,
6790
6920
  options: {
6921
+ className,
6791
6922
  uniqueConstraints,
6792
6923
  relationships
6793
6924
  }
@@ -40,6 +40,13 @@ interface UniqueConstraintConfig {
40
40
  }
41
41
  interface ModelOptions {
42
42
  name: string;
43
+ /**
44
+ * Optional PascalCase class name. Used by the v2 codegen to drive
45
+ * generated TypeScript class names (and relationship method names that
46
+ * derive from a target's class name). When absent, the v2 codegen
47
+ * falls back to suffix-based singularization of `name`.
48
+ */
49
+ className?: string;
43
50
  uniqueConstraints?: UniqueConstraintConfig[];
44
51
  relationships?: Record<string, RelationshipConfig>;
45
52
  }
@@ -447,6 +454,7 @@ declare class BaseModel implements StringSetChangeTracker {
447
454
  permissionHint: DocumentPermissionHint;
448
455
  }>;
449
456
  protected static documentYMaps: Map<string, Y.Map<any>>;
457
+ private static _observedStringSetMaps;
450
458
  private _localChanges;
451
459
  private _isDirty;
452
460
  private _isLoadingFromYjs;
@@ -521,7 +529,17 @@ declare class BaseModel implements StringSetChangeTracker {
521
529
  added: Record<string, any>;
522
530
  modified: Record<string, any>;
523
531
  removed: string[];
532
+ stringSetChanges: Record<string, {
533
+ additions: Set<string>;
534
+ removals: Set<string>;
535
+ }>;
524
536
  };
537
+ /**
538
+ * Apply a stringset change set to the parent record's Y.Map by mutating a
539
+ * nested Y.Map keyed by member. Migrates from a legacy plain-object value
540
+ * if the field hasn't been touched since the wire-format change in #561.
541
+ */
542
+ private applyStringSetChangeToYMap;
525
543
  /**
526
544
  * Deep equality check for comparing field values
527
545
  */
@@ -623,6 +641,20 @@ declare class BaseModel implements StringSetChangeTracker {
623
641
  * Execute a callback with automatic transaction handling for all modified models
624
642
  */
625
643
  static withTransaction<T>(callback: () => Promise<T> | T): Promise<T>;
644
+ /**
645
+ * Attach a one-shot observer to a nested-Y.Map stringset field. Calls
646
+ * notifyListeners() when the nested map's keys change (i.e. when a remote
647
+ * member arrives via Y.applyUpdate, or a local per-member set/delete).
648
+ * Idempotent — re-calling on the same Y.Map is a no-op.
649
+ */
650
+ protected static observeStringSetMapOnce(nestedMap: Y.Map<any>): void;
651
+ /**
652
+ * Walk the schema's stringset fields on `recordYMap` and observe any nested
653
+ * Y.Map values. Called from observer setup and from the parent observer body
654
+ * so newly-arrived stringset Y.Maps (local migration or remote create) get
655
+ * an observer too.
656
+ */
657
+ protected static attachStringSetObserversToRecord(recordYMap: Y.Map<any>, schema: any): void;
626
658
  /**
627
659
  * Sets up deep observation on a nested YMap to sync field-level changes to the database
628
660
  */
@@ -945,10 +977,22 @@ declare function schemaToToml(schema: DiscoveredSchema): string;
945
977
  * - Compound unique constraints are [[models.*.unique_constraints]]
946
978
  */
947
979
 
980
+ interface LoadSchemaOptions {
981
+ /**
982
+ * When true (default), throw on unknown keys at the model, field,
983
+ * relationship, and unique-constraint level. When false, unknown
984
+ * keys are silently ignored (legacy behavior).
985
+ */
986
+ strict?: boolean;
987
+ }
948
988
  /**
949
989
  * Parse a TOML string and return an array of DefinedModelSchema objects.
990
+ *
991
+ * By default operates in strict mode: unknown keys at the model, field,
992
+ * relationship, or unique-constraint level cause an error. Pass
993
+ * `{ strict: false }` to silently ignore unknown keys (legacy behavior).
950
994
  */
951
- declare function loadSchemaFromTomlString(tomlString: string): DefinedModelSchema[];
995
+ declare function loadSchemaFromTomlString(tomlString: string, options?: LoadSchemaOptions): DefinedModelSchema[];
952
996
 
953
997
  /**
954
998
  * Meta Sync — writes _meta_* YMaps into a YDoc.
package/dist/browser.d.ts CHANGED
@@ -40,6 +40,13 @@ interface UniqueConstraintConfig {
40
40
  }
41
41
  interface ModelOptions {
42
42
  name: string;
43
+ /**
44
+ * Optional PascalCase class name. Used by the v2 codegen to drive
45
+ * generated TypeScript class names (and relationship method names that
46
+ * derive from a target's class name). When absent, the v2 codegen
47
+ * falls back to suffix-based singularization of `name`.
48
+ */
49
+ className?: string;
43
50
  uniqueConstraints?: UniqueConstraintConfig[];
44
51
  relationships?: Record<string, RelationshipConfig>;
45
52
  }
@@ -447,6 +454,7 @@ declare class BaseModel implements StringSetChangeTracker {
447
454
  permissionHint: DocumentPermissionHint;
448
455
  }>;
449
456
  protected static documentYMaps: Map<string, Y.Map<any>>;
457
+ private static _observedStringSetMaps;
450
458
  private _localChanges;
451
459
  private _isDirty;
452
460
  private _isLoadingFromYjs;
@@ -521,7 +529,17 @@ declare class BaseModel implements StringSetChangeTracker {
521
529
  added: Record<string, any>;
522
530
  modified: Record<string, any>;
523
531
  removed: string[];
532
+ stringSetChanges: Record<string, {
533
+ additions: Set<string>;
534
+ removals: Set<string>;
535
+ }>;
524
536
  };
537
+ /**
538
+ * Apply a stringset change set to the parent record's Y.Map by mutating a
539
+ * nested Y.Map keyed by member. Migrates from a legacy plain-object value
540
+ * if the field hasn't been touched since the wire-format change in #561.
541
+ */
542
+ private applyStringSetChangeToYMap;
525
543
  /**
526
544
  * Deep equality check for comparing field values
527
545
  */
@@ -623,6 +641,20 @@ declare class BaseModel implements StringSetChangeTracker {
623
641
  * Execute a callback with automatic transaction handling for all modified models
624
642
  */
625
643
  static withTransaction<T>(callback: () => Promise<T> | T): Promise<T>;
644
+ /**
645
+ * Attach a one-shot observer to a nested-Y.Map stringset field. Calls
646
+ * notifyListeners() when the nested map's keys change (i.e. when a remote
647
+ * member arrives via Y.applyUpdate, or a local per-member set/delete).
648
+ * Idempotent — re-calling on the same Y.Map is a no-op.
649
+ */
650
+ protected static observeStringSetMapOnce(nestedMap: Y.Map<any>): void;
651
+ /**
652
+ * Walk the schema's stringset fields on `recordYMap` and observe any nested
653
+ * Y.Map values. Called from observer setup and from the parent observer body
654
+ * so newly-arrived stringset Y.Maps (local migration or remote create) get
655
+ * an observer too.
656
+ */
657
+ protected static attachStringSetObserversToRecord(recordYMap: Y.Map<any>, schema: any): void;
626
658
  /**
627
659
  * Sets up deep observation on a nested YMap to sync field-level changes to the database
628
660
  */
@@ -945,10 +977,22 @@ declare function schemaToToml(schema: DiscoveredSchema): string;
945
977
  * - Compound unique constraints are [[models.*.unique_constraints]]
946
978
  */
947
979
 
980
+ interface LoadSchemaOptions {
981
+ /**
982
+ * When true (default), throw on unknown keys at the model, field,
983
+ * relationship, and unique-constraint level. When false, unknown
984
+ * keys are silently ignored (legacy behavior).
985
+ */
986
+ strict?: boolean;
987
+ }
948
988
  /**
949
989
  * Parse a TOML string and return an array of DefinedModelSchema objects.
990
+ *
991
+ * By default operates in strict mode: unknown keys at the model, field,
992
+ * relationship, or unique-constraint level cause an error. Pass
993
+ * `{ strict: false }` to silently ignore unknown keys (legacy behavior).
950
994
  */
951
- declare function loadSchemaFromTomlString(tomlString: string): DefinedModelSchema[];
995
+ declare function loadSchemaFromTomlString(tomlString: string, options?: LoadSchemaOptions): DefinedModelSchema[];
952
996
 
953
997
  /**
954
998
  * Meta Sync — writes _meta_* YMaps into a YDoc.