js-bao 0.2.9 → 0.2.11

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/codegen.cjs CHANGED
@@ -1007,7 +1007,7 @@ ${methods.map((method) => ` ${method}`).join("\n")}
1007
1007
  // package.json
1008
1008
  var package_default = {
1009
1009
  name: "js-bao",
1010
- version: "0.2.9",
1010
+ version: "0.2.11",
1011
1011
  description: "A library providing data modeling capabilities which support live updates and queries.",
1012
1012
  types: "dist/index.d.ts",
1013
1013
  type: "module",
@@ -1067,7 +1067,7 @@ var package_default = {
1067
1067
  },
1068
1068
  dependencies: {
1069
1069
  "async-mutex": "^0.5.0",
1070
- "sql.js": "^1.13.0",
1070
+ "sql.js": "~1.13.0",
1071
1071
  ulid: "^3.0.0"
1072
1072
  },
1073
1073
  devDependencies: {
package/dist/index.cjs CHANGED
@@ -1310,7 +1310,7 @@ var init_BaseModel = __esm({
1310
1310
  Object.setPrototypeOf(this, _RecordNotFoundError.prototype);
1311
1311
  }
1312
1312
  };
1313
- SCHEMA_ACCESSORS_KEY = Symbol("jsBaoSchemaAccessors");
1313
+ SCHEMA_ACCESSORS_KEY = /* @__PURE__ */ Symbol("jsBaoSchemaAccessors");
1314
1314
  BaseModel = class _BaseModel {
1315
1315
  static modelName;
1316
1316
  static listenersMap = /* @__PURE__ */ new Map();
@@ -2093,6 +2093,114 @@ var init_BaseModel = __esm({
2093
2093
  Logger.verbose(
2094
2094
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
2095
2095
  );
2096
+ const buildUniqueKey = (recordData, fields) => {
2097
+ const keyParts = [];
2098
+ for (const field of fields) {
2099
+ const value = recordData instanceof Y.Map ? recordData.get(field) : recordData[field];
2100
+ if (value === null || value === void 0) {
2101
+ return null;
2102
+ }
2103
+ keyParts.push(String(value));
2104
+ }
2105
+ return keyParts.join("|");
2106
+ };
2107
+ const extractItemData = (key, recordData) => {
2108
+ let itemData;
2109
+ if (recordData instanceof Y.Map) {
2110
+ itemData = {};
2111
+ const unknownFields = [];
2112
+ for (const [fieldKey, value] of recordData.entries()) {
2113
+ const fieldOptions = schema.fields.get(fieldKey);
2114
+ if (!fieldOptions) {
2115
+ unknownFields.push(fieldKey);
2116
+ continue;
2117
+ }
2118
+ if (fieldOptions.type === "stringset") {
2119
+ continue;
2120
+ }
2121
+ if (value !== void 0) {
2122
+ itemData[fieldKey] = value;
2123
+ }
2124
+ }
2125
+ if (unknownFields.length > 0) {
2126
+ Logger.warn(
2127
+ `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2128
+ ", "
2129
+ )}] when syncing record ${key} from document ${docId}`
2130
+ );
2131
+ }
2132
+ } else {
2133
+ const unknownFields = [];
2134
+ const filteredData = {};
2135
+ for (const [fieldKey, value] of Object.entries(recordData)) {
2136
+ const fieldOptions = schema.fields.get(fieldKey);
2137
+ if (!fieldOptions) {
2138
+ unknownFields.push(fieldKey);
2139
+ continue;
2140
+ }
2141
+ if (fieldOptions.type === "stringset") {
2142
+ continue;
2143
+ }
2144
+ if (value !== void 0) {
2145
+ filteredData[fieldKey] = value;
2146
+ }
2147
+ }
2148
+ if (unknownFields.length > 0) {
2149
+ Logger.warn(
2150
+ `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2151
+ ", "
2152
+ )}] when syncing legacy record ${key} from document ${docId}`
2153
+ );
2154
+ }
2155
+ itemData = filteredData;
2156
+ }
2157
+ if (!itemData.id) return null;
2158
+ return itemData;
2159
+ };
2160
+ const resolveConflictsForBatch = (candidateRecords) => {
2161
+ if (schema.resolvedUniqueConstraints.length === 0) {
2162
+ return /* @__PURE__ */ new Set();
2163
+ }
2164
+ const recordIdsToDiscard = /* @__PURE__ */ new Set();
2165
+ const recordIdsToKeep = /* @__PURE__ */ new Set();
2166
+ const allRecords = /* @__PURE__ */ new Map();
2167
+ for (const [recordId, recordData] of documentYMap.entries()) {
2168
+ if (recordData && !candidateRecords.has(recordId)) {
2169
+ allRecords.set(recordId, recordData);
2170
+ }
2171
+ }
2172
+ for (const [recordId, recordData] of candidateRecords.entries()) {
2173
+ allRecords.set(recordId, recordData);
2174
+ }
2175
+ for (const constraint of schema.resolvedUniqueConstraints) {
2176
+ const recordsByUniqueKey = /* @__PURE__ */ new Map();
2177
+ for (const [recordId, recordData] of allRecords.entries()) {
2178
+ if (recordIdsToDiscard.has(recordId)) continue;
2179
+ const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2180
+ if (uniqueKey === null) continue;
2181
+ if (!recordsByUniqueKey.has(uniqueKey)) {
2182
+ recordsByUniqueKey.set(uniqueKey, []);
2183
+ }
2184
+ recordsByUniqueKey.get(uniqueKey).push(recordId);
2185
+ }
2186
+ for (const [uniqueKey, recordIds] of recordsByUniqueKey.entries()) {
2187
+ if (recordIds.length <= 1) continue;
2188
+ Logger.warn(
2189
+ `[${this.name}] CRDT conflict detected for unique constraint '${constraint.name}' on key '${uniqueKey.substring(0, 50)}${uniqueKey.length > 50 ? "..." : ""}': ${recordIds.length} records found.`
2190
+ );
2191
+ recordIds.sort();
2192
+ const idToKeep = recordIds[recordIds.length - 1];
2193
+ recordIdsToKeep.add(idToKeep);
2194
+ for (let i = 0; i < recordIds.length - 1; i++) {
2195
+ recordIdsToDiscard.add(recordIds[i]);
2196
+ }
2197
+ }
2198
+ }
2199
+ for (const idToKeep of recordIdsToKeep) {
2200
+ recordIdsToDiscard.delete(idToKeep);
2201
+ }
2202
+ return recordIdsToDiscard;
2203
+ };
2096
2204
  documentYMap.observe(async (event) => {
2097
2205
  Logger.verbose(
2098
2206
  `[${this.name}] Document YMap change detected for ${modelName}/${docId}:`,
@@ -2105,89 +2213,31 @@ var init_BaseModel = __esm({
2105
2213
  );
2106
2214
  return;
2107
2215
  }
2216
+ const isRemoteChange = !event.transaction.local;
2217
+ const remoteAdds = /* @__PURE__ */ new Map();
2218
+ const localAddsAndUpdates = [];
2108
2219
  for (const [key, change] of event.changes.keys.entries()) {
2109
2220
  const recordData = documentYMap.get(key);
2110
2221
  if (change.action === "add" || change.action === "update") {
2111
2222
  if (!recordData || !key) continue;
2112
- let itemData;
2113
- if (recordData instanceof Y.Map) {
2114
- itemData = {};
2115
- const unknownFields = [];
2116
- for (const [fieldKey, value] of recordData.entries()) {
2117
- const fieldOptions = schema.fields.get(fieldKey);
2118
- if (!fieldOptions) {
2119
- unknownFields.push(fieldKey);
2120
- continue;
2121
- }
2122
- if (fieldOptions.type === "stringset") {
2123
- continue;
2124
- }
2125
- if (value !== void 0) {
2126
- itemData[fieldKey] = value;
2127
- }
2128
- }
2129
- if (unknownFields.length > 0) {
2130
- Logger.warn(
2131
- `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2132
- ", "
2133
- )}] when syncing record ${key} from document ${docId}`
2134
- );
2135
- }
2136
- if (change.action === "add") {
2137
- Logger.verbose(
2138
- `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2139
- );
2140
- this.setupNestedYMapObserverForDocument(
2141
- key,
2142
- recordData,
2143
- docId,
2144
- permissionHint
2145
- );
2146
- }
2147
- } else {
2148
- const unknownFields = [];
2149
- const filteredData = {};
2150
- for (const [fieldKey, value] of Object.entries(recordData)) {
2151
- const fieldOptions = schema.fields.get(fieldKey);
2152
- if (!fieldOptions) {
2153
- unknownFields.push(fieldKey);
2154
- continue;
2155
- }
2156
- if (fieldOptions.type === "stringset") {
2157
- continue;
2158
- }
2159
- if (value !== void 0) {
2160
- filteredData[fieldKey] = value;
2161
- }
2162
- }
2163
- if (unknownFields.length > 0) {
2164
- Logger.warn(
2165
- `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2166
- ", "
2167
- )}] when syncing legacy record ${key} from document ${docId}`
2168
- );
2169
- }
2170
- itemData = filteredData;
2171
- }
2172
- if (!itemData.id) continue;
2173
- try {
2223
+ const itemData = extractItemData(key, recordData);
2224
+ if (!itemData) continue;
2225
+ if (change.action === "add" && recordData instanceof Y.Map) {
2174
2226
  Logger.verbose(
2175
- `[${this.name}] Syncing item to db from document ${docId} (${modelName}):`,
2176
- itemData
2227
+ `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2177
2228
  );
2178
- await currentDbInstance.insert(modelName, {
2179
- ...itemData,
2180
- type: modelName,
2181
- _meta_doc_id: docId,
2182
- _meta_permission_hint: permissionHint
2183
- });
2184
- } catch (error) {
2185
- Logger.error(
2186
- `[${this.name}] Error syncing item to db from document ${docId} (${modelName}):`,
2187
- error,
2188
- itemData
2229
+ this.setupNestedYMapObserverForDocument(
2230
+ key,
2231
+ recordData,
2232
+ docId,
2233
+ permissionHint
2189
2234
  );
2190
2235
  }
2236
+ if (isRemoteChange && change.action === "add") {
2237
+ remoteAdds.set(key, { recordData, itemData });
2238
+ } else {
2239
+ localAddsAndUpdates.push({ key, action: change.action, recordData, itemData });
2240
+ }
2191
2241
  } else if (change.action === "delete") {
2192
2242
  Logger.verbose(
2193
2243
  `[${this.name}] Deleting item from db (${modelName}/${docId}):`,
@@ -2204,6 +2254,110 @@ var init_BaseModel = __esm({
2204
2254
  }
2205
2255
  }
2206
2256
  }
2257
+ for (const { itemData } of localAddsAndUpdates) {
2258
+ try {
2259
+ Logger.verbose(
2260
+ `[${this.name}] Syncing local item to db from document ${docId} (${modelName}):`,
2261
+ itemData
2262
+ );
2263
+ await currentDbInstance.insert(modelName, {
2264
+ ...itemData,
2265
+ type: modelName,
2266
+ _meta_doc_id: docId,
2267
+ _meta_permission_hint: permissionHint
2268
+ });
2269
+ } catch (error) {
2270
+ Logger.error(
2271
+ `[${this.name}] Error syncing local item to db from document ${docId} (${modelName}):`,
2272
+ error,
2273
+ itemData
2274
+ );
2275
+ }
2276
+ }
2277
+ if (remoteAdds.size > 0) {
2278
+ Logger.verbose(
2279
+ `[${this.name}] Processing ${remoteAdds.size} remote add(s) with conflict resolution for ${modelName}/${docId}`
2280
+ );
2281
+ const candidateRecords = /* @__PURE__ */ new Map();
2282
+ for (const [key, { recordData }] of remoteAdds.entries()) {
2283
+ candidateRecords.set(key, recordData);
2284
+ }
2285
+ const idsToDiscard = resolveConflictsForBatch(candidateRecords);
2286
+ if (idsToDiscard.size > 0) {
2287
+ Logger.info(
2288
+ `[${this.name}] Discarding ${idsToDiscard.size} duplicate record(s) from remote sync for ${modelName}/${docId}: ${Array.from(idsToDiscard).join(", ")}`
2289
+ );
2290
+ }
2291
+ for (const [key, { itemData }] of remoteAdds.entries()) {
2292
+ if (idsToDiscard.has(key)) {
2293
+ Logger.verbose(
2294
+ `[${this.name}] Skipping SQLite insert for discarded duplicate: ${key}`
2295
+ );
2296
+ continue;
2297
+ }
2298
+ try {
2299
+ Logger.verbose(
2300
+ `[${this.name}] Syncing remote item to db from document ${docId} (${modelName}):`,
2301
+ itemData
2302
+ );
2303
+ await currentDbInstance.insert(modelName, {
2304
+ ...itemData,
2305
+ type: modelName,
2306
+ _meta_doc_id: docId,
2307
+ _meta_permission_hint: permissionHint
2308
+ });
2309
+ } catch (error) {
2310
+ Logger.error(
2311
+ `[${this.name}] Error syncing remote item to db from document ${docId} (${modelName}):`,
2312
+ error,
2313
+ itemData
2314
+ );
2315
+ }
2316
+ }
2317
+ if (idsToDiscard.size > 0) {
2318
+ yDoc.transact(() => {
2319
+ for (const idToDiscard of idsToDiscard) {
2320
+ const recordData = documentYMap.get(idToDiscard);
2321
+ if (!recordData) continue;
2322
+ for (const constraint of schema.resolvedUniqueConstraints) {
2323
+ const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2324
+ if (uniqueKey === null) continue;
2325
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2326
+ const constraintMap = yDoc.getMap(constraintMapName);
2327
+ const currentIndexValue = constraintMap.get(uniqueKey);
2328
+ if (currentIndexValue === idToDiscard) {
2329
+ constraintMap.delete(uniqueKey);
2330
+ Logger.verbose(
2331
+ `[${this.name}] Removed discarded record ${idToDiscard} from unique index ${constraintMapName}`
2332
+ );
2333
+ }
2334
+ }
2335
+ documentYMap.delete(idToDiscard);
2336
+ Logger.verbose(
2337
+ `[${this.name}] Removed discarded record ${idToDiscard} from Y.Doc`
2338
+ );
2339
+ }
2340
+ }, `conflict-resolution-${modelName}-${docId}`);
2341
+ yDoc.transact(() => {
2342
+ for (const constraint of schema.resolvedUniqueConstraints) {
2343
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2344
+ const constraintMap = yDoc.getMap(constraintMapName);
2345
+ for (const [recordId, recordData] of documentYMap.entries()) {
2346
+ if (!recordData) continue;
2347
+ const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2348
+ if (uniqueKey === null) continue;
2349
+ const currentIndexValue = constraintMap.get(uniqueKey);
2350
+ if (currentIndexValue !== recordId) {
2351
+ constraintMap.set(uniqueKey, recordId);
2352
+ Logger.verbose(
2353
+ `[${this.name}] Updated unique index ${constraintMapName}['${uniqueKey.substring(0, 30)}...'] to point to ${recordId}`
2354
+ );
2355
+ }
2356
+ }
2357
+ }
2358
+ }, `update-indexes-${modelName}-${docId}`);
2359
+ }
2360
+ }
2207
2361
  Logger.verbose(
2208
2362
  `[${this.name}] Document YMap observation transaction for ${modelName}/${docId} completed.`
2209
2363
  );