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/browser.cjs +230 -76
- package/dist/browser.js +230 -76
- package/dist/codegen.cjs +2 -2
- package/dist/index.cjs +230 -76
- package/dist/index.js +230 -76
- package/dist/node.cjs +230 -76
- package/dist/node.js +230 -76
- package/package.json +2 -2
package/dist/browser.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
|
-
|
|
2113
|
-
if (
|
|
2114
|
-
|
|
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}]
|
|
2176
|
-
itemData
|
|
2227
|
+
`[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
|
|
2177
2228
|
);
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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
|
);
|
package/dist/browser.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
|
-
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
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}]
|
|
2154
|
-
itemData
|
|
2205
|
+
`[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
|
|
2155
2206
|
);
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
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
|
);
|