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/node.js CHANGED
@@ -1288,7 +1288,7 @@ var init_BaseModel = __esm({
1288
1288
  Object.setPrototypeOf(this, _RecordNotFoundError.prototype);
1289
1289
  }
1290
1290
  };
1291
- SCHEMA_ACCESSORS_KEY = Symbol("jsBaoSchemaAccessors");
1291
+ SCHEMA_ACCESSORS_KEY = /* @__PURE__ */ Symbol("jsBaoSchemaAccessors");
1292
1292
  BaseModel = class _BaseModel {
1293
1293
  static modelName;
1294
1294
  static listenersMap = /* @__PURE__ */ new Map();
@@ -2071,6 +2071,114 @@ var init_BaseModel = __esm({
2071
2071
  Logger.verbose(
2072
2072
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
2073
2073
  );
2074
+ const buildUniqueKey = (recordData, fields) => {
2075
+ const keyParts = [];
2076
+ for (const field of fields) {
2077
+ const value = recordData instanceof Y.Map ? recordData.get(field) : recordData[field];
2078
+ if (value === null || value === void 0) {
2079
+ return null;
2080
+ }
2081
+ keyParts.push(String(value));
2082
+ }
2083
+ return keyParts.join("|");
2084
+ };
2085
+ const extractItemData = (key, recordData) => {
2086
+ let itemData;
2087
+ if (recordData instanceof Y.Map) {
2088
+ itemData = {};
2089
+ const unknownFields = [];
2090
+ for (const [fieldKey, value] of recordData.entries()) {
2091
+ const fieldOptions = schema.fields.get(fieldKey);
2092
+ if (!fieldOptions) {
2093
+ unknownFields.push(fieldKey);
2094
+ continue;
2095
+ }
2096
+ if (fieldOptions.type === "stringset") {
2097
+ continue;
2098
+ }
2099
+ if (value !== void 0) {
2100
+ itemData[fieldKey] = value;
2101
+ }
2102
+ }
2103
+ if (unknownFields.length > 0) {
2104
+ Logger.warn(
2105
+ `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2106
+ ", "
2107
+ )}] when syncing record ${key} from document ${docId}`
2108
+ );
2109
+ }
2110
+ } else {
2111
+ const unknownFields = [];
2112
+ const filteredData = {};
2113
+ for (const [fieldKey, value] of Object.entries(recordData)) {
2114
+ const fieldOptions = schema.fields.get(fieldKey);
2115
+ if (!fieldOptions) {
2116
+ unknownFields.push(fieldKey);
2117
+ continue;
2118
+ }
2119
+ if (fieldOptions.type === "stringset") {
2120
+ continue;
2121
+ }
2122
+ if (value !== void 0) {
2123
+ filteredData[fieldKey] = value;
2124
+ }
2125
+ }
2126
+ if (unknownFields.length > 0) {
2127
+ Logger.warn(
2128
+ `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2129
+ ", "
2130
+ )}] when syncing legacy record ${key} from document ${docId}`
2131
+ );
2132
+ }
2133
+ itemData = filteredData;
2134
+ }
2135
+ if (!itemData.id) return null;
2136
+ return itemData;
2137
+ };
2138
+ const resolveConflictsForBatch = (candidateRecords) => {
2139
+ if (schema.resolvedUniqueConstraints.length === 0) {
2140
+ return /* @__PURE__ */ new Set();
2141
+ }
2142
+ const recordIdsToDiscard = /* @__PURE__ */ new Set();
2143
+ const recordIdsToKeep = /* @__PURE__ */ new Set();
2144
+ const allRecords = /* @__PURE__ */ new Map();
2145
+ for (const [recordId, recordData] of documentYMap.entries()) {
2146
+ if (recordData && !candidateRecords.has(recordId)) {
2147
+ allRecords.set(recordId, recordData);
2148
+ }
2149
+ }
2150
+ for (const [recordId, recordData] of candidateRecords.entries()) {
2151
+ allRecords.set(recordId, recordData);
2152
+ }
2153
+ for (const constraint of schema.resolvedUniqueConstraints) {
2154
+ const recordsByUniqueKey = /* @__PURE__ */ new Map();
2155
+ for (const [recordId, recordData] of allRecords.entries()) {
2156
+ if (recordIdsToDiscard.has(recordId)) continue;
2157
+ const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2158
+ if (uniqueKey === null) continue;
2159
+ if (!recordsByUniqueKey.has(uniqueKey)) {
2160
+ recordsByUniqueKey.set(uniqueKey, []);
2161
+ }
2162
+ recordsByUniqueKey.get(uniqueKey).push(recordId);
2163
+ }
2164
+ for (const [uniqueKey, recordIds] of recordsByUniqueKey.entries()) {
2165
+ if (recordIds.length <= 1) continue;
2166
+ Logger.warn(
2167
+ `[${this.name}] CRDT conflict detected for unique constraint '${constraint.name}' on key '${uniqueKey.substring(0, 50)}${uniqueKey.length > 50 ? "..." : ""}': ${recordIds.length} records found.`
2168
+ );
2169
+ recordIds.sort();
2170
+ const idToKeep = recordIds[recordIds.length - 1];
2171
+ recordIdsToKeep.add(idToKeep);
2172
+ for (let i = 0; i < recordIds.length - 1; i++) {
2173
+ recordIdsToDiscard.add(recordIds[i]);
2174
+ }
2175
+ }
2176
+ }
2177
+ for (const idToKeep of recordIdsToKeep) {
2178
+ recordIdsToDiscard.delete(idToKeep);
2179
+ }
2180
+ return recordIdsToDiscard;
2181
+ };
2074
2182
  documentYMap.observe(async (event) => {
2075
2183
  Logger.verbose(
2076
2184
  `[${this.name}] Document YMap change detected for ${modelName}/${docId}:`,
@@ -2083,89 +2191,31 @@ var init_BaseModel = __esm({
2083
2191
  );
2084
2192
  return;
2085
2193
  }
2194
+ const isRemoteChange = !event.transaction.local;
2195
+ const remoteAdds = /* @__PURE__ */ new Map();
2196
+ const localAddsAndUpdates = [];
2086
2197
  for (const [key, change] of event.changes.keys.entries()) {
2087
2198
  const recordData = documentYMap.get(key);
2088
2199
  if (change.action === "add" || change.action === "update") {
2089
2200
  if (!recordData || !key) continue;
2090
- let itemData;
2091
- if (recordData instanceof Y.Map) {
2092
- itemData = {};
2093
- const unknownFields = [];
2094
- for (const [fieldKey, value] of recordData.entries()) {
2095
- const fieldOptions = schema.fields.get(fieldKey);
2096
- if (!fieldOptions) {
2097
- unknownFields.push(fieldKey);
2098
- continue;
2099
- }
2100
- if (fieldOptions.type === "stringset") {
2101
- continue;
2102
- }
2103
- if (value !== void 0) {
2104
- itemData[fieldKey] = value;
2105
- }
2106
- }
2107
- if (unknownFields.length > 0) {
2108
- Logger.warn(
2109
- `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2110
- ", "
2111
- )}] when syncing record ${key} from document ${docId}`
2112
- );
2113
- }
2114
- if (change.action === "add") {
2115
- Logger.verbose(
2116
- `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2117
- );
2118
- this.setupNestedYMapObserverForDocument(
2119
- key,
2120
- recordData,
2121
- docId,
2122
- permissionHint
2123
- );
2124
- }
2125
- } else {
2126
- const unknownFields = [];
2127
- const filteredData = {};
2128
- for (const [fieldKey, value] of Object.entries(recordData)) {
2129
- const fieldOptions = schema.fields.get(fieldKey);
2130
- if (!fieldOptions) {
2131
- unknownFields.push(fieldKey);
2132
- continue;
2133
- }
2134
- if (fieldOptions.type === "stringset") {
2135
- continue;
2136
- }
2137
- if (value !== void 0) {
2138
- filteredData[fieldKey] = value;
2139
- }
2140
- }
2141
- if (unknownFields.length > 0) {
2142
- Logger.warn(
2143
- `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2144
- ", "
2145
- )}] when syncing legacy record ${key} from document ${docId}`
2146
- );
2147
- }
2148
- itemData = filteredData;
2149
- }
2150
- if (!itemData.id) continue;
2151
- try {
2201
+ const itemData = extractItemData(key, recordData);
2202
+ if (!itemData) continue;
2203
+ if (change.action === "add" && recordData instanceof Y.Map) {
2152
2204
  Logger.verbose(
2153
- `[${this.name}] Syncing item to db from document ${docId} (${modelName}):`,
2154
- itemData
2205
+ `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2155
2206
  );
2156
- await currentDbInstance.insert(modelName, {
2157
- ...itemData,
2158
- type: modelName,
2159
- _meta_doc_id: docId,
2160
- _meta_permission_hint: permissionHint
2161
- });
2162
- } catch (error) {
2163
- Logger.error(
2164
- `[${this.name}] Error syncing item to db from document ${docId} (${modelName}):`,
2165
- error,
2166
- itemData
2207
+ this.setupNestedYMapObserverForDocument(
2208
+ key,
2209
+ recordData,
2210
+ docId,
2211
+ permissionHint
2167
2212
  );
2168
2213
  }
2214
+ if (isRemoteChange && change.action === "add") {
2215
+ remoteAdds.set(key, { recordData, itemData });
2216
+ } else {
2217
+ localAddsAndUpdates.push({ key, action: change.action, recordData, itemData });
2218
+ }
2169
2219
  } else if (change.action === "delete") {
2170
2220
  Logger.verbose(
2171
2221
  `[${this.name}] Deleting item from db (${modelName}/${docId}):`,
@@ -2182,6 +2232,110 @@ var init_BaseModel = __esm({
2182
2232
  }
2183
2233
  }
2184
2234
  }
2235
+ for (const { itemData } of localAddsAndUpdates) {
2236
+ try {
2237
+ Logger.verbose(
2238
+ `[${this.name}] Syncing local item to db from document ${docId} (${modelName}):`,
2239
+ itemData
2240
+ );
2241
+ await currentDbInstance.insert(modelName, {
2242
+ ...itemData,
2243
+ type: modelName,
2244
+ _meta_doc_id: docId,
2245
+ _meta_permission_hint: permissionHint
2246
+ });
2247
+ } catch (error) {
2248
+ Logger.error(
2249
+ `[${this.name}] Error syncing local item to db from document ${docId} (${modelName}):`,
2250
+ error,
2251
+ itemData
2252
+ );
2253
+ }
2254
+ }
2255
+ if (remoteAdds.size > 0) {
2256
+ Logger.verbose(
2257
+ `[${this.name}] Processing ${remoteAdds.size} remote add(s) with conflict resolution for ${modelName}/${docId}`
2258
+ );
2259
+ const candidateRecords = /* @__PURE__ */ new Map();
2260
+ for (const [key, { recordData }] of remoteAdds.entries()) {
2261
+ candidateRecords.set(key, recordData);
2262
+ }
2263
+ const idsToDiscard = resolveConflictsForBatch(candidateRecords);
2264
+ if (idsToDiscard.size > 0) {
2265
+ Logger.info(
2266
+ `[${this.name}] Discarding ${idsToDiscard.size} duplicate record(s) from remote sync for ${modelName}/${docId}: ${Array.from(idsToDiscard).join(", ")}`
2267
+ );
2268
+ }
2269
+ for (const [key, { itemData }] of remoteAdds.entries()) {
2270
+ if (idsToDiscard.has(key)) {
2271
+ Logger.verbose(
2272
+ `[${this.name}] Skipping SQLite insert for discarded duplicate: ${key}`
2273
+ );
2274
+ continue;
2275
+ }
2276
+ try {
2277
+ Logger.verbose(
2278
+ `[${this.name}] Syncing remote item to db from document ${docId} (${modelName}):`,
2279
+ itemData
2280
+ );
2281
+ await currentDbInstance.insert(modelName, {
2282
+ ...itemData,
2283
+ type: modelName,
2284
+ _meta_doc_id: docId,
2285
+ _meta_permission_hint: permissionHint
2286
+ });
2287
+ } catch (error) {
2288
+ Logger.error(
2289
+ `[${this.name}] Error syncing remote item to db from document ${docId} (${modelName}):`,
2290
+ error,
2291
+ itemData
2292
+ );
2293
+ }
2294
+ }
2295
+ if (idsToDiscard.size > 0) {
2296
+ yDoc.transact(() => {
2297
+ for (const idToDiscard of idsToDiscard) {
2298
+ const recordData = documentYMap.get(idToDiscard);
2299
+ if (!recordData) continue;
2300
+ for (const constraint of schema.resolvedUniqueConstraints) {
2301
+ const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2302
+ if (uniqueKey === null) continue;
2303
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2304
+ const constraintMap = yDoc.getMap(constraintMapName);
2305
+ const currentIndexValue = constraintMap.get(uniqueKey);
2306
+ if (currentIndexValue === idToDiscard) {
2307
+ constraintMap.delete(uniqueKey);
2308
+ Logger.verbose(
2309
+ `[${this.name}] Removed discarded record ${idToDiscard} from unique index ${constraintMapName}`
2310
+ );
2311
+ }
2312
+ }
2313
+ documentYMap.delete(idToDiscard);
2314
+ Logger.verbose(
2315
+ `[${this.name}] Removed discarded record ${idToDiscard} from Y.Doc`
2316
+ );
2317
+ }
2318
+ }, `conflict-resolution-${modelName}-${docId}`);
2319
+ yDoc.transact(() => {
2320
+ for (const constraint of schema.resolvedUniqueConstraints) {
2321
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2322
+ const constraintMap = yDoc.getMap(constraintMapName);
2323
+ for (const [recordId, recordData] of documentYMap.entries()) {
2324
+ if (!recordData) continue;
2325
+ const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2326
+ if (uniqueKey === null) continue;
2327
+ const currentIndexValue = constraintMap.get(uniqueKey);
2328
+ if (currentIndexValue !== recordId) {
2329
+ constraintMap.set(uniqueKey, recordId);
2330
+ Logger.verbose(
2331
+ `[${this.name}] Updated unique index ${constraintMapName}['${uniqueKey.substring(0, 30)}...'] to point to ${recordId}`
2332
+ );
2333
+ }
2334
+ }
2335
+ }
2336
+ }, `update-indexes-${modelName}-${docId}`);
2337
+ }
2338
+ }
2185
2339
  Logger.verbose(
2186
2340
  `[${this.name}] Document YMap observation transaction for ${modelName}/${docId} completed.`
2187
2341
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-bao",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "A library providing data modeling capabilities which support live updates and queries.",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "async-mutex": "^0.5.0",
63
- "sql.js": "^1.13.0",
63
+ "sql.js": "~1.13.0",
64
64
  "ulid": "^3.0.0"
65
65
  },
66
66
  "devDependencies": {