@xnetjs/data 0.12.0 → 1.0.0
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/account-ZD3HMRY6.js +12 -0
- package/dist/{account-ledger-R2GAFJL6.js → account-ledger-ZHBNVREV.js} +3 -3
- package/dist/auth/index.d.ts +15 -5
- package/dist/auth/index.js +1 -1
- package/dist/budget-FTGZEXX6.js +12 -0
- package/dist/canvas-NQBTRNLS.js +10 -0
- package/dist/channel-474QAZGP.js +12 -0
- package/dist/chat-message-LGLCITLE.js +10 -0
- package/dist/{chunk-VQ7JHB67.js → chunk-22FKOLMX.js} +2 -2
- package/dist/{chunk-LYSWLCOI.js → chunk-26HTTVJN.js} +2 -2
- package/dist/{chunk-PH3PVAR6.js → chunk-3PV6QTKF.js} +2 -2
- package/dist/{chunk-KQUALW4O.js → chunk-4KJ4NAMC.js} +1 -1
- package/dist/{chunk-U64CW73O.js → chunk-52DR23IW.js} +2 -2
- package/dist/{chunk-SVNGSZZA.js → chunk-57UZYV3L.js} +2 -2
- package/dist/{chunk-ZMZSBJBC.js → chunk-5B47M2IG.js} +175 -175
- package/dist/{chunk-QVQVKZ3U.js → chunk-5UJMY3AH.js} +6 -5
- package/dist/{chunk-3J3HILXO.js → chunk-7PCMTV6J.js} +2 -2
- package/dist/{chunk-YSUIXD2Y.js → chunk-C4CQH7UD.js} +2 -2
- package/dist/{chunk-JFMLCFZJ.js → chunk-CARPMPXV.js} +285 -229
- package/dist/{chunk-PNESGUH5.js → chunk-D4EVJN2P.js} +2 -2
- package/dist/{chunk-RL64OJJ5.js → chunk-D4LUUZYD.js} +36 -9
- package/dist/{chunk-W7EBL7ZU.js → chunk-DP4Q2HMO.js} +2 -2
- package/dist/{chunk-5EXHNXPL.js → chunk-DPJ4T2LQ.js} +2 -2
- package/dist/{chunk-BQLQSWKZ.js → chunk-EN6XTVLZ.js} +2 -2
- package/dist/{chunk-Q67VT5XL.js → chunk-ENA6NEFY.js} +5 -5
- package/dist/{chunk-TORDOOJ5.js → chunk-EYRZ6ZUB.js} +1 -1
- package/dist/{chunk-NI4FHG2K.js → chunk-EZTBWWMS.js} +2 -2
- package/dist/{chunk-UQM3G5A2.js → chunk-FWCM2TKJ.js} +2 -2
- package/dist/{chunk-OSAWNZVM.js → chunk-GCTONBF6.js} +1 -1
- package/dist/{chunk-2ZNJWIQG.js → chunk-HPUXHSKR.js} +2 -2
- package/dist/{chunk-ZPZ7XDA6.js → chunk-HTZTBXKL.js} +2 -2
- package/dist/{chunk-MCKP4F72.js → chunk-I7JMGCV6.js} +26 -5
- package/dist/{chunk-OGJCRNGE.js → chunk-IA23CLGV.js} +2 -2
- package/dist/{chunk-YEFKQYKA.js → chunk-IKGUBSZP.js} +2 -2
- package/dist/{chunk-XMSKJ5PV.js → chunk-IKI5FBNO.js} +2 -2
- package/dist/{chunk-OCMSAKWV.js → chunk-JA56EQJO.js} +15 -3
- package/dist/{chunk-TCTZW4A6.js → chunk-JLUFPUIL.js} +2 -2
- package/dist/{chunk-WEPK7SZF.js → chunk-KTPSQJ5L.js} +2 -2
- package/dist/{chunk-T5AZAOG5.js → chunk-LK5ZIAIE.js} +1 -1
- package/dist/{chunk-XROI44ZP.js → chunk-M4KGY2PC.js} +2 -2
- package/dist/{chunk-QJW5LDP4.js → chunk-NSMSOIEW.js} +2 -2
- package/dist/{chunk-FB63TZVI.js → chunk-OF6455OV.js} +2 -2
- package/dist/{chunk-6OMG4I7M.js → chunk-OIWW5TG3.js} +2 -2
- package/dist/{chunk-6KLGA6IR.js → chunk-Q6VTTPG4.js} +2 -2
- package/dist/{chunk-J4ANIXIV.js → chunk-QDLWTOII.js} +2 -2
- package/dist/{chunk-EBNMV2VO.js → chunk-RRXJNZX5.js} +1 -1
- package/dist/{chunk-FVEACSKE.js → chunk-RXBLWQQI.js} +2 -2
- package/dist/{chunk-L7NRPLE5.js → chunk-S6JWYPVW.js} +2 -2
- package/dist/{chunk-JDFNMFOZ.js → chunk-SOXXBQ4P.js} +2 -2
- package/dist/{chunk-4EYKGTSQ.js → chunk-T45PWXNA.js} +2 -2
- package/dist/{chunk-MD553LDL.js → chunk-TRPXLCND.js} +2 -2
- package/dist/{chunk-ZCOFZY5M.js → chunk-UUQVNDBG.js} +2 -2
- package/dist/{chunk-53F4PRNC.js → chunk-W3QOEPUN.js} +2 -2
- package/dist/{chunk-S6U6TCMN.js → chunk-WC7EDJHC.js} +2 -2
- package/dist/{chunk-KQAT4XBL.js → chunk-WWQO7QKV.js} +2 -2
- package/dist/{chunk-BY5O6LNC.js → chunk-XJBDESCX.js} +1 -1
- package/dist/{chunk-ZZ6TWKGS.js → chunk-XQDXQ7WG.js} +2 -2
- package/dist/{chunk-MBTUO3ZL.js → chunk-YD2ZPFT7.js} +2 -2
- package/dist/{chunk-ZLU4Y3O2.js → chunk-YHO7GNZK.js} +2 -2
- package/dist/{chunk-Y3S5SRVM.js → chunk-YSD4NSCE.js} +2 -2
- package/dist/{chunk-IJ44LR7N.js → chunk-ZKJXGQCR.js} +2 -2
- package/dist/{chunk-GU6THOAB.js → chunk-ZKMWJNDQ.js} +2 -2
- package/dist/{chunk-OPPHF3TF.js → chunk-ZKVRGKLM.js} +3 -3
- package/dist/{clone-CbmoWwu4.d.ts → clone-Bj0nQyMc.d.ts} +2 -2
- package/dist/comment-IAZJHG6G.js +10 -0
- package/dist/{crm-L5RYAAF3.js → crm-AFJSSPHK.js} +5 -5
- package/dist/dashboard-2DLFZM2J.js +10 -0
- package/dist/database/index.d.ts +3 -3
- package/dist/database/index.js +10 -10
- package/dist/database-5M7ZA33U.js +10 -0
- package/dist/database-field-IFUWOK7X.js +10 -0
- package/dist/database-row-C52KCOPB.js +10 -0
- package/dist/database-select-option-AEAPKXEE.js +10 -0
- package/dist/database-view-GUGJHS7J.js +10 -0
- package/dist/experiment-JXHENWXV.js +10 -0
- package/dist/{external-item-P2E5X7BZ.js → external-item-ZVD33IXV.js} +5 -5
- package/dist/external-reference-LGFNVEKC.js +8 -0
- package/dist/feed-CLEHVA6Q.js +12 -0
- package/dist/feed-item-QXRIE6FU.js +12 -0
- package/dist/{folder-6KM3KYD5.js → folder-NU5TJFOT.js} +3 -3
- package/dist/{game-5AK5XWBR.js → game-7WW2JX6O.js} +5 -5
- package/dist/grant-W772T6FE.js +8 -0
- package/dist/{grant-expiration-cleaner-Bm1fSG42.d.ts → grant-expiration-cleaner-BrMW0ffX.d.ts} +7 -2
- package/dist/import-batch-ZRU4FY6Y.js +12 -0
- package/dist/{inbox-state-E5NSVOGP.js → inbox-state-IO5C7HVE.js} +3 -3
- package/dist/index.d.ts +7 -6
- package/dist/index.js +316 -314
- package/dist/map-BZAVGCR6.js +10 -0
- package/dist/media-asset-VHFL65HI.js +8 -0
- package/dist/{meeting-LMUYTZBF.js → meeting-PVNKBGTI.js} +5 -5
- package/dist/{memory-PNRTMBZO.js → memory-5PD3XCKB.js} +3 -3
- package/dist/metric-6IGGFJMB.js +10 -0
- package/dist/milestone-MZAZCWGR.js +12 -0
- package/dist/{moderation-FFHETMRB.js → moderation-CXQB77CP.js} +3 -3
- package/dist/observation-O3DLKVIH.js +10 -0
- package/dist/page-UE5NPYEF.js +10 -0
- package/dist/posting-5VZZMOKS.js +12 -0
- package/dist/{profile-XBPZQA3C.js → profile-USKJ6PN7.js} +3 -3
- package/dist/project-UHGGEFLP.js +10 -0
- package/dist/{query-ast-Bvg0Wolx.d.ts → query-ast-ESRn8U2H.d.ts} +1 -1
- package/dist/reaction-KD3G75UH.js +10 -0
- package/dist/{registry-_49rC6IG.d.ts → registry-BGD80QCd.d.ts} +1 -1
- package/dist/saved-view-AZTUINZA.js +10 -0
- package/dist/schema/index.d.ts +5 -5
- package/dist/schema/index.js +70 -70
- package/dist/{schema-extension-LNRF2YRD.js → schema-extension-DV5D7JCQ.js} +5 -5
- package/dist/{space-IJU24PBH.js → space-THD2MWHR.js} +6 -6
- package/dist/{space-membership-DEUMM7NV.js → space-membership-M3QFMONE.js} +3 -3
- package/dist/store/index.d.ts +13 -4
- package/dist/store/index.js +4 -4
- package/dist/{store-DEWnip2G.d.ts → store-B0fb6WdJ.d.ts} +1 -1
- package/dist/{system-ILHCZOVO.js → system-LFIMY3QQ.js} +3 -3
- package/dist/{tag-SKCHM3U5.js → tag-W7ZYPXHK.js} +3 -3
- package/dist/{task-7AOBP7WC.js → task-DHTPD66H.js} +5 -5
- package/dist/task-view-J2WRE62C.js +10 -0
- package/dist/transaction-ANNDXCJM.js +12 -0
- package/dist/transcription-DFTQ4FQ4.js +12 -0
- package/dist/{types-BB8-SLEa.d.ts → types-B5rrBwRI.d.ts} +17 -0
- package/dist/user-widget-5IRVORF3.js +8 -0
- package/dist/workspace-RC4V4IF3.js +8 -0
- package/package.json +7 -7
- package/dist/account-MWNCH4GJ.js +0 -12
- package/dist/budget-NN7P35XP.js +0 -12
- package/dist/canvas-6WYBNH53.js +0 -10
- package/dist/channel-AT3LQF4A.js +0 -12
- package/dist/chat-message-47LETTYF.js +0 -10
- package/dist/comment-NIMFL4XB.js +0 -10
- package/dist/dashboard-JMKTTRA7.js +0 -10
- package/dist/database-GIXSPZJW.js +0 -10
- package/dist/database-field-3EPAMZUR.js +0 -10
- package/dist/database-row-5MEIOJ52.js +0 -10
- package/dist/database-select-option-BBBJFLLH.js +0 -10
- package/dist/database-view-DM7YCTZT.js +0 -10
- package/dist/experiment-DOVOZ34Y.js +0 -10
- package/dist/external-reference-7BSFF6SA.js +0 -8
- package/dist/feed-2IXHHP6O.js +0 -12
- package/dist/feed-item-DPE6LN7I.js +0 -12
- package/dist/grant-WTQSNT5A.js +0 -8
- package/dist/import-batch-IXBJLQPJ.js +0 -12
- package/dist/map-RG5Y3CPC.js +0 -10
- package/dist/media-asset-QTCGLIZA.js +0 -8
- package/dist/metric-65SVOX5C.js +0 -10
- package/dist/milestone-XEO6ZGDG.js +0 -12
- package/dist/observation-2Y3QKUXM.js +0 -10
- package/dist/page-2V27JGKJ.js +0 -10
- package/dist/posting-GBOSZCMZ.js +0 -12
- package/dist/project-CQL5LN26.js +0 -10
- package/dist/reaction-T2DTJBBV.js +0 -10
- package/dist/saved-view-4R2DBCL3.js +0 -10
- package/dist/task-view-NUGKELJP.js +0 -10
- package/dist/transaction-ZDSEGDIR.js +0 -12
- package/dist/transcription-PSCLDAGX.js +0 -12
- package/dist/user-widget-RLY2N46A.js +0 -8
- package/dist/workspace-WDKSQMOK.js +0 -8
|
@@ -1,18 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SYSTEM_SCHEMA_BASE_IRIS,
|
|
3
|
+
isSystemNamespaceResource,
|
|
4
|
+
isSystemSchemaIri
|
|
5
|
+
} from "./chunk-XJBDESCX.js";
|
|
1
6
|
import {
|
|
2
7
|
applyNodeQueryDescriptor,
|
|
3
8
|
getNodeQuerySearchTokens,
|
|
4
9
|
withoutNodeQueryMaterializedView,
|
|
5
10
|
withoutNodeQueryPagination
|
|
6
11
|
} from "./chunk-S5RP5RKY.js";
|
|
7
|
-
import {
|
|
8
|
-
SYSTEM_SCHEMA_BASE_IRIS,
|
|
9
|
-
isSystemNamespaceResource,
|
|
10
|
-
isSystemSchemaIri
|
|
11
|
-
} from "./chunk-BY5O6LNC.js";
|
|
12
12
|
import {
|
|
13
13
|
createNodeId,
|
|
14
14
|
getBaseSchemaIRI
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-RRXJNZX5.js";
|
|
16
16
|
|
|
17
17
|
// src/store/query-ast.ts
|
|
18
18
|
var QUERY_AST_VERSION = 1;
|
|
@@ -887,187 +887,23 @@ function evaluateQueryASTPlannerGate(value) {
|
|
|
887
887
|
}
|
|
888
888
|
|
|
889
889
|
// src/store/store.ts
|
|
890
|
-
import {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
schemaId: op.options.schemaId,
|
|
908
|
-
properties: op.options.properties
|
|
909
|
-
};
|
|
910
|
-
change = await host.createBatchedChange(
|
|
911
|
-
"node-change",
|
|
912
|
-
payload,
|
|
913
|
-
input.lamport,
|
|
914
|
-
input.now,
|
|
915
|
-
input.batchId,
|
|
916
|
-
i,
|
|
917
|
-
input.batchSize,
|
|
918
|
-
input.storage
|
|
919
|
-
);
|
|
920
|
-
await host.applyChange(change, input.storage);
|
|
921
|
-
result = await input.storage.getNode(id);
|
|
922
|
-
await host.persistEncryptedNodeSnapshot(result, input.storage);
|
|
923
|
-
break;
|
|
924
|
-
}
|
|
925
|
-
case "update": {
|
|
926
|
-
const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
|
|
927
|
-
if (!existing) {
|
|
928
|
-
throw new Error(`Node not found: ${op.nodeId}`);
|
|
929
|
-
}
|
|
930
|
-
previousNode = existing;
|
|
931
|
-
const payload = {
|
|
932
|
-
nodeId: op.nodeId,
|
|
933
|
-
properties: op.options.properties
|
|
934
|
-
};
|
|
935
|
-
change = await host.createBatchedChange(
|
|
936
|
-
"node-change",
|
|
937
|
-
payload,
|
|
938
|
-
input.lamport,
|
|
939
|
-
input.now,
|
|
940
|
-
input.batchId,
|
|
941
|
-
i,
|
|
942
|
-
input.batchSize,
|
|
943
|
-
input.storage
|
|
944
|
-
);
|
|
945
|
-
await host.applyChange(change, input.storage);
|
|
946
|
-
result = await input.storage.getNode(op.nodeId);
|
|
947
|
-
await host.persistEncryptedNodeSnapshot(result, input.storage);
|
|
948
|
-
break;
|
|
949
|
-
}
|
|
950
|
-
case "delete": {
|
|
951
|
-
const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
|
|
952
|
-
if (!existing) {
|
|
953
|
-
throw new Error(`Node not found: ${op.nodeId}`);
|
|
954
|
-
}
|
|
955
|
-
previousNode = existing;
|
|
956
|
-
const payload = {
|
|
957
|
-
nodeId: op.nodeId,
|
|
958
|
-
properties: {},
|
|
959
|
-
deleted: true
|
|
960
|
-
};
|
|
961
|
-
change = await host.createBatchedChange(
|
|
962
|
-
"node-change",
|
|
963
|
-
payload,
|
|
964
|
-
input.lamport,
|
|
965
|
-
input.now,
|
|
966
|
-
input.batchId,
|
|
967
|
-
i,
|
|
968
|
-
input.batchSize,
|
|
969
|
-
input.storage
|
|
970
|
-
);
|
|
971
|
-
await host.applyChange(change, input.storage);
|
|
972
|
-
result = null;
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
case "restore": {
|
|
976
|
-
const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
|
|
977
|
-
if (!existing) {
|
|
978
|
-
throw new Error(`Node not found: ${op.nodeId}`);
|
|
979
|
-
}
|
|
980
|
-
previousNode = existing;
|
|
981
|
-
const payload = {
|
|
982
|
-
nodeId: op.nodeId,
|
|
983
|
-
properties: {},
|
|
984
|
-
deleted: false
|
|
985
|
-
};
|
|
986
|
-
change = await host.createBatchedChange(
|
|
987
|
-
"node-change",
|
|
988
|
-
payload,
|
|
989
|
-
input.lamport,
|
|
990
|
-
input.now,
|
|
991
|
-
input.batchId,
|
|
992
|
-
i,
|
|
993
|
-
input.batchSize,
|
|
994
|
-
input.storage
|
|
995
|
-
);
|
|
996
|
-
await host.applyChange(change, input.storage);
|
|
997
|
-
result = await input.storage.getNode(op.nodeId);
|
|
998
|
-
await host.persistEncryptedNodeSnapshot(result, input.storage);
|
|
999
|
-
break;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
changes.push(change);
|
|
1003
|
-
results.push(result);
|
|
1004
|
-
events.push({ change, result, previousNode });
|
|
1005
|
-
}
|
|
1006
|
-
return { results, changes, events };
|
|
1007
|
-
}
|
|
1008
|
-
async function executeTransactionOperationsFast(host, input) {
|
|
1009
|
-
const planned = input.operations.map((op) => ({
|
|
1010
|
-
op,
|
|
1011
|
-
id: op.type === "create" ? op.options.id ?? createNodeId() : op.nodeId
|
|
1012
|
-
}));
|
|
1013
|
-
const preflight = await host.getBatchPreflight(
|
|
1014
|
-
planned.map((entry) => entry.id),
|
|
1015
|
-
host.storage
|
|
1016
|
-
);
|
|
1017
|
-
const nodesById = host.cloneNodeMap(preflight.nodesById);
|
|
1018
|
-
const lastChanges = new Map(preflight.lastChangesByNodeId);
|
|
1019
|
-
const results = [];
|
|
1020
|
-
const changes = [];
|
|
1021
|
-
const events = [];
|
|
1022
|
-
const affectedSchemaIds = /* @__PURE__ */ new Set();
|
|
1023
|
-
for (let i = 0; i < planned.length; i++) {
|
|
1024
|
-
const { op, id } = planned[i];
|
|
1025
|
-
const existing = nodesById.get(id) ?? null;
|
|
1026
|
-
if (op.type !== "create" && !existing) {
|
|
1027
|
-
throw new Error(`Node not found: ${id}`);
|
|
1028
|
-
}
|
|
1029
|
-
const payload = op.type === "create" ? { nodeId: id, schemaId: op.options.schemaId, properties: op.options.properties } : op.type === "update" ? { nodeId: id, properties: op.options.properties } : { nodeId: id, properties: {}, deleted: op.type === "delete" };
|
|
1030
|
-
const change = await host.createBatchedChangeWithParentHash(
|
|
1031
|
-
"node-change",
|
|
1032
|
-
payload,
|
|
1033
|
-
lastChanges.get(id)?.hash ?? null,
|
|
1034
|
-
input.lamport,
|
|
1035
|
-
input.now,
|
|
1036
|
-
input.batchId,
|
|
1037
|
-
i,
|
|
1038
|
-
input.batchSize
|
|
1039
|
-
);
|
|
1040
|
-
const schemaId = existing?.schemaId ?? (op.type === "create" ? op.options.schemaId : null);
|
|
1041
|
-
if (!schemaId) {
|
|
1042
|
-
throw new Error(`First change for node ${id} must include schemaId`);
|
|
1043
|
-
}
|
|
1044
|
-
const node = host.materializeNodeChange(
|
|
1045
|
-
change,
|
|
1046
|
-
existing ?? host.createInitialNodeFromChange(change, schemaId)
|
|
1047
|
-
);
|
|
1048
|
-
nodesById.set(id, node);
|
|
1049
|
-
lastChanges.set(id, change);
|
|
1050
|
-
affectedSchemaIds.add(schemaId);
|
|
1051
|
-
changes.push(change);
|
|
1052
|
-
const result = op.type === "delete" ? null : node;
|
|
1053
|
-
results.push(result);
|
|
1054
|
-
events.push({ change, result, previousNode: existing });
|
|
1055
|
-
}
|
|
1056
|
-
const touchedIds = Array.from(new Set(planned.map((entry) => entry.id)));
|
|
1057
|
-
await host.storage.applyNodeBatch({
|
|
1058
|
-
batchId: input.batchId,
|
|
1059
|
-
nodes: touchedIds.flatMap((id) => {
|
|
1060
|
-
const node = nodesById.get(id);
|
|
1061
|
-
return node ? [node] : [];
|
|
1062
|
-
}),
|
|
1063
|
-
changes,
|
|
1064
|
-
lastLamportTime: host.clockTime(),
|
|
1065
|
-
affectedSchemaIds: Array.from(affectedSchemaIds),
|
|
1066
|
-
indexMode: "touched",
|
|
1067
|
-
indexProperties: true
|
|
1068
|
-
});
|
|
1069
|
-
return { results, changes, events };
|
|
1070
|
-
}
|
|
890
|
+
import {
|
|
891
|
+
LWW_TIEBREAK_KEY_VERSION,
|
|
892
|
+
compareChangeApplicationOrder,
|
|
893
|
+
computeLwwTiebreakKey,
|
|
894
|
+
lwwWins
|
|
895
|
+
} from "@xnetjs/core";
|
|
896
|
+
import { base64ToBytes, bytesToBase64 } from "@xnetjs/crypto";
|
|
897
|
+
import { parseDID } from "@xnetjs/identity";
|
|
898
|
+
import {
|
|
899
|
+
createLamportClock,
|
|
900
|
+
tick,
|
|
901
|
+
receive,
|
|
902
|
+
signChange,
|
|
903
|
+
createUnsignedChange,
|
|
904
|
+
createBatchId
|
|
905
|
+
} from "@xnetjs/sync";
|
|
906
|
+
import { verifyChange, verifyChangeHash } from "@xnetjs/sync";
|
|
1071
907
|
|
|
1072
908
|
// src/store/batch-write-orchestrator.ts
|
|
1073
909
|
var elapsedMs = (startedAt) => Math.max(0, Date.now() - startedAt);
|
|
@@ -1159,19 +995,6 @@ async function executeDeterministicNodeImport(host, input) {
|
|
|
1159
995
|
};
|
|
1160
996
|
}
|
|
1161
997
|
|
|
1162
|
-
// src/store/store.ts
|
|
1163
|
-
import { base64ToBytes, bytesToBase64 } from "@xnetjs/crypto";
|
|
1164
|
-
import { parseDID } from "@xnetjs/identity";
|
|
1165
|
-
import {
|
|
1166
|
-
createLamportClock,
|
|
1167
|
-
tick,
|
|
1168
|
-
receive,
|
|
1169
|
-
signChange,
|
|
1170
|
-
createUnsignedChange,
|
|
1171
|
-
createBatchId
|
|
1172
|
-
} from "@xnetjs/sync";
|
|
1173
|
-
import { verifyChange, verifyChangeHash } from "@xnetjs/sync";
|
|
1174
|
-
|
|
1175
998
|
// src/store/permission-error.ts
|
|
1176
999
|
import { TaggedError } from "@xnetjs/core";
|
|
1177
1000
|
var PermissionError = class extends TaggedError {
|
|
@@ -1364,6 +1187,186 @@ function findSchemaIdForNode(operations, nodeId, tempIds) {
|
|
|
1364
1187
|
return void 0;
|
|
1365
1188
|
}
|
|
1366
1189
|
|
|
1190
|
+
// src/store/transaction-executor.ts
|
|
1191
|
+
async function executeTransactionOperations(host, input) {
|
|
1192
|
+
const results = [];
|
|
1193
|
+
const changes = [];
|
|
1194
|
+
const events = [];
|
|
1195
|
+
for (let i = 0; i < input.operations.length; i++) {
|
|
1196
|
+
const op = input.operations[i];
|
|
1197
|
+
let change;
|
|
1198
|
+
let result = null;
|
|
1199
|
+
let previousNode = null;
|
|
1200
|
+
switch (op.type) {
|
|
1201
|
+
case "create": {
|
|
1202
|
+
const id = op.options.id ?? createNodeId();
|
|
1203
|
+
const payload = {
|
|
1204
|
+
nodeId: id,
|
|
1205
|
+
schemaId: op.options.schemaId,
|
|
1206
|
+
properties: op.options.properties
|
|
1207
|
+
};
|
|
1208
|
+
change = await host.createBatchedChange(
|
|
1209
|
+
"node-change",
|
|
1210
|
+
payload,
|
|
1211
|
+
input.lamport,
|
|
1212
|
+
input.now,
|
|
1213
|
+
input.batchId,
|
|
1214
|
+
i,
|
|
1215
|
+
input.batchSize,
|
|
1216
|
+
input.storage
|
|
1217
|
+
);
|
|
1218
|
+
await host.applyChange(change, input.storage);
|
|
1219
|
+
result = await input.storage.getNode(id);
|
|
1220
|
+
await host.persistEncryptedNodeSnapshot(result, input.storage);
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
case "update": {
|
|
1224
|
+
const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
|
|
1225
|
+
if (!existing) {
|
|
1226
|
+
throw new Error(`Node not found: ${op.nodeId}`);
|
|
1227
|
+
}
|
|
1228
|
+
previousNode = existing;
|
|
1229
|
+
const payload = {
|
|
1230
|
+
nodeId: op.nodeId,
|
|
1231
|
+
properties: op.options.properties
|
|
1232
|
+
};
|
|
1233
|
+
change = await host.createBatchedChange(
|
|
1234
|
+
"node-change",
|
|
1235
|
+
payload,
|
|
1236
|
+
input.lamport,
|
|
1237
|
+
input.now,
|
|
1238
|
+
input.batchId,
|
|
1239
|
+
i,
|
|
1240
|
+
input.batchSize,
|
|
1241
|
+
input.storage
|
|
1242
|
+
);
|
|
1243
|
+
await host.applyChange(change, input.storage);
|
|
1244
|
+
result = await input.storage.getNode(op.nodeId);
|
|
1245
|
+
await host.persistEncryptedNodeSnapshot(result, input.storage);
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
case "delete": {
|
|
1249
|
+
const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
|
|
1250
|
+
if (!existing) {
|
|
1251
|
+
throw new Error(`Node not found: ${op.nodeId}`);
|
|
1252
|
+
}
|
|
1253
|
+
previousNode = existing;
|
|
1254
|
+
const payload = {
|
|
1255
|
+
nodeId: op.nodeId,
|
|
1256
|
+
properties: {},
|
|
1257
|
+
deleted: true
|
|
1258
|
+
};
|
|
1259
|
+
change = await host.createBatchedChange(
|
|
1260
|
+
"node-change",
|
|
1261
|
+
payload,
|
|
1262
|
+
input.lamport,
|
|
1263
|
+
input.now,
|
|
1264
|
+
input.batchId,
|
|
1265
|
+
i,
|
|
1266
|
+
input.batchSize,
|
|
1267
|
+
input.storage
|
|
1268
|
+
);
|
|
1269
|
+
await host.applyChange(change, input.storage);
|
|
1270
|
+
result = null;
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
case "restore": {
|
|
1274
|
+
const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
|
|
1275
|
+
if (!existing) {
|
|
1276
|
+
throw new Error(`Node not found: ${op.nodeId}`);
|
|
1277
|
+
}
|
|
1278
|
+
previousNode = existing;
|
|
1279
|
+
const payload = {
|
|
1280
|
+
nodeId: op.nodeId,
|
|
1281
|
+
properties: {},
|
|
1282
|
+
deleted: false
|
|
1283
|
+
};
|
|
1284
|
+
change = await host.createBatchedChange(
|
|
1285
|
+
"node-change",
|
|
1286
|
+
payload,
|
|
1287
|
+
input.lamport,
|
|
1288
|
+
input.now,
|
|
1289
|
+
input.batchId,
|
|
1290
|
+
i,
|
|
1291
|
+
input.batchSize,
|
|
1292
|
+
input.storage
|
|
1293
|
+
);
|
|
1294
|
+
await host.applyChange(change, input.storage);
|
|
1295
|
+
result = await input.storage.getNode(op.nodeId);
|
|
1296
|
+
await host.persistEncryptedNodeSnapshot(result, input.storage);
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
changes.push(change);
|
|
1301
|
+
results.push(result);
|
|
1302
|
+
events.push({ change, result, previousNode });
|
|
1303
|
+
}
|
|
1304
|
+
return { results, changes, events };
|
|
1305
|
+
}
|
|
1306
|
+
async function executeTransactionOperationsFast(host, input) {
|
|
1307
|
+
const planned = input.operations.map((op) => ({
|
|
1308
|
+
op,
|
|
1309
|
+
id: op.type === "create" ? op.options.id ?? createNodeId() : op.nodeId
|
|
1310
|
+
}));
|
|
1311
|
+
const preflight = await host.getBatchPreflight(
|
|
1312
|
+
planned.map((entry) => entry.id),
|
|
1313
|
+
host.storage
|
|
1314
|
+
);
|
|
1315
|
+
const nodesById = host.cloneNodeMap(preflight.nodesById);
|
|
1316
|
+
const lastChanges = new Map(preflight.lastChangesByNodeId);
|
|
1317
|
+
const results = [];
|
|
1318
|
+
const changes = [];
|
|
1319
|
+
const events = [];
|
|
1320
|
+
const affectedSchemaIds = /* @__PURE__ */ new Set();
|
|
1321
|
+
for (let i = 0; i < planned.length; i++) {
|
|
1322
|
+
const { op, id } = planned[i];
|
|
1323
|
+
const existing = nodesById.get(id) ?? null;
|
|
1324
|
+
if (op.type !== "create" && !existing) {
|
|
1325
|
+
throw new Error(`Node not found: ${id}`);
|
|
1326
|
+
}
|
|
1327
|
+
const payload = op.type === "create" ? { nodeId: id, schemaId: op.options.schemaId, properties: op.options.properties } : op.type === "update" ? { nodeId: id, properties: op.options.properties } : { nodeId: id, properties: {}, deleted: op.type === "delete" };
|
|
1328
|
+
const change = await host.createBatchedChangeWithParentHash(
|
|
1329
|
+
"node-change",
|
|
1330
|
+
payload,
|
|
1331
|
+
lastChanges.get(id)?.hash ?? null,
|
|
1332
|
+
input.lamport,
|
|
1333
|
+
input.now,
|
|
1334
|
+
input.batchId,
|
|
1335
|
+
i,
|
|
1336
|
+
input.batchSize
|
|
1337
|
+
);
|
|
1338
|
+
const schemaId = existing?.schemaId ?? (op.type === "create" ? op.options.schemaId : null);
|
|
1339
|
+
if (!schemaId) {
|
|
1340
|
+
throw new Error(`First change for node ${id} must include schemaId`);
|
|
1341
|
+
}
|
|
1342
|
+
const node = host.materializeNodeChange(
|
|
1343
|
+
change,
|
|
1344
|
+
existing ?? host.createInitialNodeFromChange(change, schemaId)
|
|
1345
|
+
);
|
|
1346
|
+
nodesById.set(id, node);
|
|
1347
|
+
lastChanges.set(id, change);
|
|
1348
|
+
affectedSchemaIds.add(schemaId);
|
|
1349
|
+
changes.push(change);
|
|
1350
|
+
const result = op.type === "delete" ? null : node;
|
|
1351
|
+
results.push(result);
|
|
1352
|
+
events.push({ change, result, previousNode: existing });
|
|
1353
|
+
}
|
|
1354
|
+
const touchedIds = Array.from(new Set(planned.map((entry) => entry.id)));
|
|
1355
|
+
await host.storage.applyNodeBatch({
|
|
1356
|
+
batchId: input.batchId,
|
|
1357
|
+
nodes: touchedIds.flatMap((id) => {
|
|
1358
|
+
const node = nodesById.get(id);
|
|
1359
|
+
return node ? [node] : [];
|
|
1360
|
+
}),
|
|
1361
|
+
changes,
|
|
1362
|
+
lastLamportTime: host.clockTime(),
|
|
1363
|
+
affectedSchemaIds: Array.from(affectedSchemaIds),
|
|
1364
|
+
indexMode: "touched",
|
|
1365
|
+
indexProperties: true
|
|
1366
|
+
});
|
|
1367
|
+
return { results, changes, events };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1367
1370
|
// src/store/store.ts
|
|
1368
1371
|
var MAX_CONFLICTS = 200;
|
|
1369
1372
|
var ENCRYPTED_NODE_MARKER = "xnet:node-encrypted:v1";
|
|
@@ -1463,7 +1466,7 @@ var NodeStore = class {
|
|
|
1463
1466
|
schemaId: options.schemaId,
|
|
1464
1467
|
properties: options.properties
|
|
1465
1468
|
},
|
|
1466
|
-
authAction: "
|
|
1469
|
+
authAction: "create",
|
|
1467
1470
|
requireExisting: false,
|
|
1468
1471
|
createSchemaId: options.schemaId
|
|
1469
1472
|
});
|
|
@@ -1477,7 +1480,7 @@ var NodeStore = class {
|
|
|
1477
1480
|
}
|
|
1478
1481
|
await this.assertAuthorized({
|
|
1479
1482
|
subject: this.authorDID,
|
|
1480
|
-
action: "
|
|
1483
|
+
action: "create",
|
|
1481
1484
|
nodeId: id,
|
|
1482
1485
|
patch: options.properties,
|
|
1483
1486
|
node: {
|
|
@@ -1611,7 +1614,7 @@ var NodeStore = class {
|
|
|
1611
1614
|
nodeId: id,
|
|
1612
1615
|
properties: options.properties
|
|
1613
1616
|
},
|
|
1614
|
-
authAction: "
|
|
1617
|
+
authAction: "update",
|
|
1615
1618
|
requireExisting: true
|
|
1616
1619
|
});
|
|
1617
1620
|
this.emit(change2, node2, previousNode, false);
|
|
@@ -1635,7 +1638,7 @@ var NodeStore = class {
|
|
|
1635
1638
|
}
|
|
1636
1639
|
await this.assertAuthorized({
|
|
1637
1640
|
subject: this.authorDID,
|
|
1638
|
-
action: "
|
|
1641
|
+
action: "update",
|
|
1639
1642
|
nodeId: id,
|
|
1640
1643
|
patch: options.properties,
|
|
1641
1644
|
node: {
|
|
@@ -1758,7 +1761,7 @@ var NodeStore = class {
|
|
|
1758
1761
|
properties: {},
|
|
1759
1762
|
deleted: false
|
|
1760
1763
|
},
|
|
1761
|
-
authAction: "
|
|
1764
|
+
authAction: "update",
|
|
1762
1765
|
requireExisting: true
|
|
1763
1766
|
});
|
|
1764
1767
|
this.emit(change2, node2, previousNode, false);
|
|
@@ -1771,7 +1774,7 @@ var NodeStore = class {
|
|
|
1771
1774
|
}
|
|
1772
1775
|
await this.assertAuthorized({
|
|
1773
1776
|
subject: this.authorDID,
|
|
1774
|
-
action: "
|
|
1777
|
+
action: "update",
|
|
1775
1778
|
nodeId: id,
|
|
1776
1779
|
node: {
|
|
1777
1780
|
schemaId: existing.schemaId,
|
|
@@ -2472,12 +2475,21 @@ var NodeStore = class {
|
|
|
2472
2475
|
);
|
|
2473
2476
|
}
|
|
2474
2477
|
if (this.authEvaluator) {
|
|
2475
|
-
const
|
|
2478
|
+
const stored = await this.storage.getNode(change.payload.nodeId);
|
|
2479
|
+
const action = this.inferActionFromChange(change, stored);
|
|
2476
2480
|
const decision = await this.authEvaluator.can({
|
|
2477
2481
|
subject: change.authorDID,
|
|
2478
2482
|
action,
|
|
2479
2483
|
nodeId: change.payload.nodeId,
|
|
2480
|
-
patch: action === "
|
|
2484
|
+
patch: action === "create" || action === "update" ? change.payload.properties : void 0,
|
|
2485
|
+
// A remote create has no stored node to evaluate against — build the
|
|
2486
|
+
// draft from the payload so container-relation roles resolve
|
|
2487
|
+
// (exploration 0304). Update/delete load the stored node as before.
|
|
2488
|
+
node: action === "create" && change.payload.schemaId ? {
|
|
2489
|
+
schemaId: change.payload.schemaId,
|
|
2490
|
+
createdBy: change.authorDID,
|
|
2491
|
+
properties: change.payload.properties
|
|
2492
|
+
} : void 0
|
|
2481
2493
|
});
|
|
2482
2494
|
if (!decision.allowed) {
|
|
2483
2495
|
this.telemetry?.reportSecurityEvent("data.unauthorized_remote_change", "medium");
|
|
@@ -2922,7 +2934,11 @@ var NodeStore = class {
|
|
|
2922
2934
|
const newTs = {
|
|
2923
2935
|
lamport: change.lamport,
|
|
2924
2936
|
author: change.authorDID,
|
|
2925
|
-
wallTime: change.wallTime
|
|
2937
|
+
wallTime: change.wallTime,
|
|
2938
|
+
// Grinding-resistant final tiebreak (exploration 0305): only v4+ changes
|
|
2939
|
+
// carry a key, so a v4-vs-legacy comparison degrades to the author DID
|
|
2940
|
+
// and mixed fleets still agree.
|
|
2941
|
+
...(change.protocolVersion ?? 0) >= LWW_TIEBREAK_KEY_VERSION ? { tiebreakKey: computeLwwTiebreakKey(change.authorDID, key, value) } : {}
|
|
2926
2942
|
};
|
|
2927
2943
|
const incomingWins = !existingTs || this.shouldReplace(existingTs, newTs);
|
|
2928
2944
|
const target = isUnknownProperty ? node._unknown ??= {} : node.properties;
|
|
@@ -3057,7 +3073,7 @@ var NodeStore = class {
|
|
|
3057
3073
|
if (operation.type === "create") {
|
|
3058
3074
|
await this.assertAuthorized({
|
|
3059
3075
|
subject: this.authorDID,
|
|
3060
|
-
action: "
|
|
3076
|
+
action: "create",
|
|
3061
3077
|
nodeId: operation.options.id ?? "",
|
|
3062
3078
|
node: {
|
|
3063
3079
|
schemaId: operation.options.schemaId,
|
|
@@ -3077,17 +3093,20 @@ var NodeStore = class {
|
|
|
3077
3093
|
}
|
|
3078
3094
|
await this.assertAuthorized({
|
|
3079
3095
|
subject: this.authorDID,
|
|
3080
|
-
action: "
|
|
3096
|
+
action: "update",
|
|
3081
3097
|
nodeId: operation.nodeId,
|
|
3082
3098
|
patch: operation.type === "update" ? operation.options.properties : void 0
|
|
3083
3099
|
});
|
|
3084
3100
|
}
|
|
3085
3101
|
}
|
|
3086
|
-
inferActionFromChange(change) {
|
|
3102
|
+
inferActionFromChange(change, stored) {
|
|
3087
3103
|
if (change.payload.deleted === true) {
|
|
3088
3104
|
return "delete";
|
|
3089
3105
|
}
|
|
3090
|
-
|
|
3106
|
+
if (!stored && change.payload.schemaId) {
|
|
3107
|
+
return "create";
|
|
3108
|
+
}
|
|
3109
|
+
return "update";
|
|
3091
3110
|
}
|
|
3092
3111
|
shouldRecomputeRecipients(schemaId, patch) {
|
|
3093
3112
|
const authRelevant = this.authRelevantPropertyLookup?.(schemaId);
|
|
@@ -3520,7 +3539,10 @@ function hydrateJoinedRows(rows) {
|
|
|
3520
3539
|
node.timestamps[row.property_key] = {
|
|
3521
3540
|
lamport: row.lamport_time ?? 0,
|
|
3522
3541
|
author: row.updated_by ?? "",
|
|
3523
|
-
wallTime: row.prop_updated_at ?? 0
|
|
3542
|
+
wallTime: row.prop_updated_at ?? 0,
|
|
3543
|
+
// Round-trip the stored key so the in-memory LWW comparison matches the
|
|
3544
|
+
// SQL guard exactly (0305); NULL legacy rows keep the author tiebreak.
|
|
3545
|
+
...row.tiebreak_key != null ? { tiebreakKey: row.tiebreak_key } : {}
|
|
3524
3546
|
};
|
|
3525
3547
|
if ((row.prop_updated_at ?? 0) >= node.updatedAt) {
|
|
3526
3548
|
node.updatedBy = row.updated_by ?? node.createdBy;
|
|
@@ -3540,7 +3562,10 @@ function hydrateAggregatedRows(rows) {
|
|
|
3540
3562
|
timestamps[key] = {
|
|
3541
3563
|
lamport: entry.l ?? 0,
|
|
3542
3564
|
author: entry.b ?? "",
|
|
3543
|
-
wallTime: entry.w ?? 0
|
|
3565
|
+
wallTime: entry.w ?? 0,
|
|
3566
|
+
// Round-trip the stored tiebreak key (0305) so aggregated hydrate
|
|
3567
|
+
// matches the SQL guard; NULL/absent → author tiebreak.
|
|
3568
|
+
...entry.k != null ? { tiebreakKey: entry.k } : {}
|
|
3544
3569
|
};
|
|
3545
3570
|
if ((entry.w ?? 0) >= row.updated_at) {
|
|
3546
3571
|
node.updatedBy = entry.b ?? row.created_by;
|
|
@@ -3571,6 +3596,7 @@ function buildHydrateChunkQuery(ids) {
|
|
|
3571
3596
|
p.lamport_time,
|
|
3572
3597
|
p.updated_by,
|
|
3573
3598
|
p.updated_at AS prop_updated_at,
|
|
3599
|
+
p.tiebreak_key,
|
|
3574
3600
|
wanted.ordinal
|
|
3575
3601
|
FROM wanted
|
|
3576
3602
|
JOIN nodes n ON n.id = wanted.id
|
|
@@ -3599,7 +3625,7 @@ function buildAggregatedHydrateChunkQuery(ids) {
|
|
|
3599
3625
|
FILTER (WHERE p.property_key IS NOT NULL) AS props_json,
|
|
3600
3626
|
json_group_object(
|
|
3601
3627
|
p.property_key,
|
|
3602
|
-
json_object('l', p.lamport_time, 'b', p.updated_by, 'w', p.updated_at)
|
|
3628
|
+
json_object('l', p.lamport_time, 'b', p.updated_by, 'w', p.updated_at, 'k', p.tiebreak_key)
|
|
3603
3629
|
) FILTER (WHERE p.property_key IS NOT NULL) AS meta_json
|
|
3604
3630
|
FROM wanted
|
|
3605
3631
|
JOIN nodes n ON n.id = wanted.id
|
|
@@ -4522,7 +4548,12 @@ var NODE_PROPERTIES_LWW_GUARD = lwwUpdateGuardSql({
|
|
|
4522
4548
|
table: "node_properties",
|
|
4523
4549
|
lamportColumn: "lamport_time",
|
|
4524
4550
|
wallTimeColumn: "updated_at",
|
|
4525
|
-
authorColumn: "updated_by"
|
|
4551
|
+
authorColumn: "updated_by",
|
|
4552
|
+
// Grinding-resistant final tiebreak (exploration 0305): larger key wins when
|
|
4553
|
+
// both rows carry one (v4+), else the author DID. The key is precomputed in
|
|
4554
|
+
// application code and stored, so SQL only compares the opaque hex — byte
|
|
4555
|
+
// identical to `shouldReplace` in ./store.ts, no user-defined function.
|
|
4556
|
+
tiebreakKeyColumn: "tiebreak_key"
|
|
4526
4557
|
});
|
|
4527
4558
|
var DEFAULT_ADAPTIVE_INDEXING = {
|
|
4528
4559
|
enabled: false,
|
|
@@ -4622,6 +4653,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4622
4653
|
nodeReadAuthorizer;
|
|
4623
4654
|
/** One-time guard: ensure the `auth_fingerprint` column exists on upgraded DBs. */
|
|
4624
4655
|
materializationColumnsReady = false;
|
|
4656
|
+
nodePropertyColumnsReady = false;
|
|
4625
4657
|
/**
|
|
4626
4658
|
* Descriptor→SQL compilation lives in `query-compiler.ts` (exploration
|
|
4627
4659
|
* 0276). Flags are read per-compile so the compiler never captures stale
|
|
@@ -4966,6 +4998,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4966
4998
|
* Used by importNodes to avoid nested transactions.
|
|
4967
4999
|
*/
|
|
4968
5000
|
async _setNodeInternal(node, options) {
|
|
5001
|
+
await this.ensureNodePropertyColumns();
|
|
4969
5002
|
await this.db.run(
|
|
4970
5003
|
`INSERT INTO nodes (id, schema_id, created_at, updated_at, created_by, deleted_at)
|
|
4971
5004
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
@@ -4987,13 +5020,14 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4987
5020
|
const timestamp = node.timestamps[key];
|
|
4988
5021
|
if (!timestamp) continue;
|
|
4989
5022
|
await this.db.run(
|
|
4990
|
-
`INSERT INTO node_properties (node_id, property_key, value, lamport_time, updated_by, updated_at)
|
|
4991
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
5023
|
+
`INSERT INTO node_properties (node_id, property_key, value, lamport_time, updated_by, updated_at, tiebreak_key)
|
|
5024
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4992
5025
|
ON CONFLICT(node_id, property_key) DO UPDATE SET
|
|
4993
5026
|
value = excluded.value,
|
|
4994
5027
|
lamport_time = excluded.lamport_time,
|
|
4995
5028
|
updated_by = excluded.updated_by,
|
|
4996
|
-
updated_at = excluded.updated_at
|
|
5029
|
+
updated_at = excluded.updated_at,
|
|
5030
|
+
tiebreak_key = excluded.tiebreak_key
|
|
4997
5031
|
WHERE ${NODE_PROPERTIES_LWW_GUARD}`,
|
|
4998
5032
|
[
|
|
4999
5033
|
node.id,
|
|
@@ -5001,7 +5035,8 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5001
5035
|
this.serializeValue(value),
|
|
5002
5036
|
timestamp.lamport,
|
|
5003
5037
|
timestamp.author,
|
|
5004
|
-
timestamp.wallTime
|
|
5038
|
+
timestamp.wallTime,
|
|
5039
|
+
timestamp.tiebreakKey ?? null
|
|
5005
5040
|
]
|
|
5006
5041
|
);
|
|
5007
5042
|
}
|
|
@@ -5063,7 +5098,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5063
5098
|
)
|
|
5064
5099
|
SELECT
|
|
5065
5100
|
ln.id, ln.schema_id, ln.created_at, ln.updated_at, ln.created_by, ln.deleted_at,
|
|
5066
|
-
p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at,
|
|
5101
|
+
p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at, p.tiebreak_key,
|
|
5067
5102
|
NULL as ordinal
|
|
5068
5103
|
FROM limited_nodes ln
|
|
5069
5104
|
LEFT JOIN node_properties p ON ln.id = p.node_id
|
|
@@ -5075,7 +5110,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5075
5110
|
sql = `
|
|
5076
5111
|
SELECT
|
|
5077
5112
|
n.id, n.schema_id, n.created_at, n.updated_at, n.created_by, n.deleted_at,
|
|
5078
|
-
p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at,
|
|
5113
|
+
p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at, p.tiebreak_key,
|
|
5079
5114
|
NULL as ordinal
|
|
5080
5115
|
FROM nodes n
|
|
5081
5116
|
LEFT JOIN node_properties p ON n.id = p.node_id
|
|
@@ -5588,6 +5623,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5588
5623
|
if (!this.db.transactionBatch) {
|
|
5589
5624
|
throw new Error("SQLite transactionBatch is not available.");
|
|
5590
5625
|
}
|
|
5626
|
+
await this.ensureNodePropertyColumns();
|
|
5591
5627
|
const operations = [];
|
|
5592
5628
|
const indexProperties = options?.indexProperties ?? true;
|
|
5593
5629
|
const hasFullTextSearch = await this.fullTextIndexing.hasTable();
|
|
@@ -5638,16 +5674,17 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5638
5674
|
const timestamp = node.timestamps[key];
|
|
5639
5675
|
if (!timestamp) continue;
|
|
5640
5676
|
operations.push({
|
|
5641
|
-
// Full LWW ordering
|
|
5642
|
-
// upsert above and `shouldReplace` in ./store.ts (0272).
|
|
5677
|
+
// Full LWW ordering chain — keep in lockstep with the setNode
|
|
5678
|
+
// upsert above and `shouldReplace` in ./store.ts (0272/0305).
|
|
5643
5679
|
sql: `INSERT INTO node_properties
|
|
5644
|
-
(node_id, property_key, value, lamport_time, updated_by, updated_at)
|
|
5645
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
5680
|
+
(node_id, property_key, value, lamport_time, updated_by, updated_at, tiebreak_key)
|
|
5681
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
5646
5682
|
ON CONFLICT(node_id, property_key) DO UPDATE SET
|
|
5647
5683
|
value = excluded.value,
|
|
5648
5684
|
lamport_time = excluded.lamport_time,
|
|
5649
5685
|
updated_by = excluded.updated_by,
|
|
5650
|
-
updated_at = excluded.updated_at
|
|
5686
|
+
updated_at = excluded.updated_at,
|
|
5687
|
+
tiebreak_key = excluded.tiebreak_key
|
|
5651
5688
|
WHERE ${NODE_PROPERTIES_LWW_GUARD}`,
|
|
5652
5689
|
params: [
|
|
5653
5690
|
node.id,
|
|
@@ -5655,7 +5692,8 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5655
5692
|
this.serializeValue(value),
|
|
5656
5693
|
timestamp.lamport,
|
|
5657
5694
|
timestamp.author,
|
|
5658
|
-
timestamp.wallTime
|
|
5695
|
+
timestamp.wallTime,
|
|
5696
|
+
timestamp.tiebreakKey ?? null
|
|
5659
5697
|
]
|
|
5660
5698
|
});
|
|
5661
5699
|
}
|
|
@@ -5907,6 +5945,24 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5907
5945
|
}
|
|
5908
5946
|
this.materializationColumnsReady = true;
|
|
5909
5947
|
}
|
|
5948
|
+
/**
|
|
5949
|
+
* Ensure the `node_properties.tiebreak_key` column exists (exploration 0305).
|
|
5950
|
+
* Fresh databases get it from the DDL; databases created before schema v8 need
|
|
5951
|
+
* this in-place add so the grinding-resistant LWW guard has a column to
|
|
5952
|
+
* compare. Idempotent, memoized, and non-fatal on races — a legacy NULL key
|
|
5953
|
+
* just falls back to the author-DID tiebreak.
|
|
5954
|
+
*/
|
|
5955
|
+
async ensureNodePropertyColumns() {
|
|
5956
|
+
if (this.nodePropertyColumnsReady) return;
|
|
5957
|
+
try {
|
|
5958
|
+
const columns = await this.db.query(`PRAGMA table_info(node_properties)`);
|
|
5959
|
+
if (!columns.some((column) => column.name === "tiebreak_key")) {
|
|
5960
|
+
await this.db.run("ALTER TABLE node_properties ADD COLUMN tiebreak_key TEXT");
|
|
5961
|
+
}
|
|
5962
|
+
} catch {
|
|
5963
|
+
}
|
|
5964
|
+
this.nodePropertyColumnsReady = true;
|
|
5965
|
+
}
|
|
5910
5966
|
setNodeReadAuthorizer(authorizer) {
|
|
5911
5967
|
this.nodeReadAuthorizer = authorizer;
|
|
5912
5968
|
}
|