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/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.
|
|
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": "
|
|
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
|
-
|
|
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
|
);
|