@xnetjs/data 0.12.0 → 2.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/checkpoint-MZHJ7PCZ.js +12 -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-JFMLCFZJ.js → chunk-4S73XDWH.js} +585 -230
- package/dist/{chunk-U64CW73O.js → chunk-52DR23IW.js} +2 -2
- package/dist/{chunk-SVNGSZZA.js → chunk-57UZYV3L.js} +2 -2
- package/dist/{chunk-QVQVKZ3U.js → chunk-5UJMY3AH.js} +6 -5
- package/dist/chunk-6ZUJID52.js +41 -0
- package/dist/{chunk-3J3HILXO.js → chunk-7PCMTV6J.js} +2 -2
- package/dist/{chunk-YSUIXD2Y.js → chunk-C4CQH7UD.js} +2 -2
- package/dist/{chunk-PNESGUH5.js → chunk-D4EVJN2P.js} +2 -2
- package/dist/{chunk-RL64OJJ5.js → chunk-D4LUUZYD.js} +36 -9
- package/dist/{chunk-Q67VT5XL.js → chunk-DESSPQBS.js} +59 -10
- 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-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-MEAIGK3W.js +58 -0
- 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-ZMZSBJBC.js → chunk-U223J425.js} +174 -170
- 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-Z5I2T75Z.js +70 -0
- 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-DNcpkYAA.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 +11 -11
- 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/debug-report-BTNZAKX3.js +10 -0
- package/dist/draft-KGHITKX2.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-1by9wRlb.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 +8 -7
- package/dist/index.js +175 -157
- 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-NV5StnHo.d.ts} +1 -1
- package/dist/reaction-KD3G75UH.js +10 -0
- package/dist/{registry-_49rC6IG.d.ts → registry-s1fYgCSj.d.ts} +1 -1
- package/dist/saved-view-AZTUINZA.js +10 -0
- package/dist/schema/index.d.ts +173 -12
- package/dist/schema/index.js +96 -80
- 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 +18 -5
- package/dist/store/index.js +4 -4
- package/dist/{store-DEWnip2G.d.ts → store-BIDKtpjW.d.ts} +56 -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-C64g-IXg.d.ts} +87 -2
- 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
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
SYSTEM_SCHEMA_BASE_IRIS,
|
|
9
9
|
isSystemNamespaceResource,
|
|
10
10
|
isSystemSchemaIri
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-XJBDESCX.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";
|
|
@@ -1390,6 +1393,14 @@ var NodeStore = class {
|
|
|
1390
1393
|
listeners = /* @__PURE__ */ new Set();
|
|
1391
1394
|
nodeListeners = /* @__PURE__ */ new Map();
|
|
1392
1395
|
batchListeners = /* @__PURE__ */ new Set();
|
|
1396
|
+
// Draft overlay (exploration 0329)
|
|
1397
|
+
checkedOutDraft = null;
|
|
1398
|
+
cloneToOriginal = /* @__PURE__ */ new Map();
|
|
1399
|
+
draftMemberSet = /* @__PURE__ */ new Set();
|
|
1400
|
+
draftDynamicMembers = false;
|
|
1401
|
+
/** null = conservative "all schemas" (memberSchemaIds omitted). */
|
|
1402
|
+
draftMemberSchemaSet = null;
|
|
1403
|
+
draftOverlayListeners = /* @__PURE__ */ new Set();
|
|
1393
1404
|
schemaLookup;
|
|
1394
1405
|
propertyLookup;
|
|
1395
1406
|
lensRegistry;
|
|
@@ -1463,12 +1474,13 @@ var NodeStore = class {
|
|
|
1463
1474
|
schemaId: options.schemaId,
|
|
1464
1475
|
properties: options.properties
|
|
1465
1476
|
},
|
|
1466
|
-
authAction: "
|
|
1477
|
+
authAction: "create",
|
|
1467
1478
|
requireExisting: false,
|
|
1468
1479
|
createSchemaId: options.schemaId
|
|
1469
1480
|
});
|
|
1470
1481
|
this.emit(change2, node2, null, false);
|
|
1471
1482
|
this.authEvaluator?.invalidate(node2.id);
|
|
1483
|
+
this.notifyDraftNodeCreated(node2.id, node2.schemaId);
|
|
1472
1484
|
if (this.telemetry) {
|
|
1473
1485
|
this.telemetry.reportPerformance("data.create", Date.now() - start);
|
|
1474
1486
|
this.telemetry.reportUsage("data.create", 1);
|
|
@@ -1477,7 +1489,7 @@ var NodeStore = class {
|
|
|
1477
1489
|
}
|
|
1478
1490
|
await this.assertAuthorized({
|
|
1479
1491
|
subject: this.authorDID,
|
|
1480
|
-
action: "
|
|
1492
|
+
action: "create",
|
|
1481
1493
|
nodeId: id,
|
|
1482
1494
|
patch: options.properties,
|
|
1483
1495
|
node: {
|
|
@@ -1504,6 +1516,7 @@ var NodeStore = class {
|
|
|
1504
1516
|
await this.persistEncryptedNodeSnapshot(node);
|
|
1505
1517
|
this.emit(change, node, null, false);
|
|
1506
1518
|
this.authEvaluator?.invalidate(node.id);
|
|
1519
|
+
this.notifyDraftNodeCreated(node.id, node.schemaId);
|
|
1507
1520
|
if (this.telemetry) {
|
|
1508
1521
|
this.telemetry.reportPerformance("data.create", Date.now() - start);
|
|
1509
1522
|
this.telemetry.reportUsage("data.create", 1);
|
|
@@ -1520,10 +1533,186 @@ var NodeStore = class {
|
|
|
1520
1533
|
* Get a Node by ID.
|
|
1521
1534
|
*/
|
|
1522
1535
|
async get(id) {
|
|
1536
|
+
const cloneId = this.checkedOutDraft?.clones[id];
|
|
1537
|
+
if (cloneId) {
|
|
1538
|
+
const clone = await this.getRaw(cloneId);
|
|
1539
|
+
if (clone) return { ...clone, id };
|
|
1540
|
+
}
|
|
1541
|
+
return this.getRaw(id);
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Read a node WITHOUT the draft overlay (exploration 0329) — merge review
|
|
1545
|
+
* and diff surfaces use this to see main's true state while checked out.
|
|
1546
|
+
*/
|
|
1547
|
+
async getRaw(id) {
|
|
1523
1548
|
const node = await this.storage.getNode(id);
|
|
1524
1549
|
const decrypted = await this.decryptNodeSnapshotIfPresent(node);
|
|
1525
1550
|
return await this.canReadNode(decrypted) ? decrypted : null;
|
|
1526
1551
|
}
|
|
1552
|
+
// ==========================================================================
|
|
1553
|
+
// Draft overlay (exploration 0329)
|
|
1554
|
+
// ==========================================================================
|
|
1555
|
+
/**
|
|
1556
|
+
* Check a draft out (or pass null to return to main). While checked out,
|
|
1557
|
+
* member reads swap to clone content under original ids, member writes
|
|
1558
|
+
* redirect to clones (lazily forking via `onMissingMember`), and clone
|
|
1559
|
+
* change events mirror to original-id subscribers.
|
|
1560
|
+
*/
|
|
1561
|
+
setCheckedOutDraft(overlay) {
|
|
1562
|
+
this.checkedOutDraft = overlay;
|
|
1563
|
+
this.cloneToOriginal = new Map(
|
|
1564
|
+
overlay ? Object.entries(overlay.clones).map(([orig, clone]) => [clone, orig]) : []
|
|
1565
|
+
);
|
|
1566
|
+
this.draftMemberSet = overlay && overlay.members !== "dynamic" ? new Set(overlay.members) : /* @__PURE__ */ new Set();
|
|
1567
|
+
this.draftDynamicMembers = overlay?.members === "dynamic";
|
|
1568
|
+
this.draftMemberSchemaSet = overlay?.memberSchemaIds ? new Set(overlay.memberSchemaIds) : null;
|
|
1569
|
+
this.notifyDraftOverlayListeners();
|
|
1570
|
+
}
|
|
1571
|
+
/** Report a locally-created node to the checkout's draft-born hook (0329 P4). */
|
|
1572
|
+
notifyDraftNodeCreated(nodeId, schemaId) {
|
|
1573
|
+
const overlay = this.checkedOutDraft;
|
|
1574
|
+
if (!overlay?.onNodeCreated) return;
|
|
1575
|
+
if (nodeId === overlay.draftId || this.cloneToOriginal.has(nodeId)) return;
|
|
1576
|
+
try {
|
|
1577
|
+
overlay.onNodeCreated(nodeId, schemaId);
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
console.error("Error in draft onNodeCreated hook:", err);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
notifyDraftOverlayListeners() {
|
|
1583
|
+
for (const listener of this.draftOverlayListeners) {
|
|
1584
|
+
try {
|
|
1585
|
+
listener();
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
console.error("Error in draft overlay listener:", err);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
getCheckedOutDraft() {
|
|
1592
|
+
return this.checkedOutDraft;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Device-local draft privacy (exploration 0329): ids marked here (draft
|
|
1596
|
+
* bookkeeping nodes, clones, draft-born nodes) are excluded from outbound
|
|
1597
|
+
* sync publication by the personal node-sync room's `shouldPublish`
|
|
1598
|
+
* predicate. In-memory only — the draft module rehydrates it from open
|
|
1599
|
+
* Draft nodes at boot, BEFORE sync starts.
|
|
1600
|
+
*/
|
|
1601
|
+
draftPrivateNodeIds = /* @__PURE__ */ new Set();
|
|
1602
|
+
markDraftPrivate(ids) {
|
|
1603
|
+
for (const id of ids) this.draftPrivateNodeIds.add(id);
|
|
1604
|
+
}
|
|
1605
|
+
unmarkDraftPrivate(ids) {
|
|
1606
|
+
for (const id of ids) this.draftPrivateNodeIds.delete(id);
|
|
1607
|
+
}
|
|
1608
|
+
isDraftPrivate(id) {
|
|
1609
|
+
return this.draftPrivateNodeIds.has(id);
|
|
1610
|
+
}
|
|
1611
|
+
/** Notified on every checkout change (including clone-map refreshes). */
|
|
1612
|
+
subscribeToDraftOverlay(listener) {
|
|
1613
|
+
this.draftOverlayListeners.add(listener);
|
|
1614
|
+
return () => this.draftOverlayListeners.delete(listener);
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Swap checked-out members' content to their clones (original ids kept, so
|
|
1618
|
+
* MV id lists and grid ordering are untouched). No-op without a checkout.
|
|
1619
|
+
*/
|
|
1620
|
+
async overlayStates(states) {
|
|
1621
|
+
const overlay = this.checkedOutDraft;
|
|
1622
|
+
if (!overlay) return states;
|
|
1623
|
+
const visible = this.cloneToOriginal.size > 0 ? states.filter((state) => !this.cloneToOriginal.has(state.id)) : states;
|
|
1624
|
+
const cloneIds = visible.map((s) => overlay.clones[s.id]).filter((cloneId) => cloneId !== void 0);
|
|
1625
|
+
if (cloneIds.length === 0) return visible;
|
|
1626
|
+
const cloneMap = await this.getNodesById(cloneIds, this.storage);
|
|
1627
|
+
return Promise.all(
|
|
1628
|
+
visible.map(async (state) => {
|
|
1629
|
+
const cloneId = overlay.clones[state.id];
|
|
1630
|
+
const clone = cloneId ? cloneMap.get(cloneId) : void 0;
|
|
1631
|
+
if (!clone) return state;
|
|
1632
|
+
const decrypted = await this.decryptNodeSnapshotIfPresent(clone);
|
|
1633
|
+
return decrypted ? { ...decrypted, id: state.id } : state;
|
|
1634
|
+
})
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Draft-aware query path (0329 P5): candidates from storage WITHOUT
|
|
1639
|
+
* pagination, unioned with checked-out members the predicate may have
|
|
1640
|
+
* missed (their clone values can change membership), content-swapped,
|
|
1641
|
+
* clone rows hidden, then the full descriptor re-applied in JS so
|
|
1642
|
+
* filter/sort/pagination reflect draft values.
|
|
1643
|
+
*/
|
|
1644
|
+
async draftAwareQuery(descriptor, start) {
|
|
1645
|
+
const overlay = this.checkedOutDraft;
|
|
1646
|
+
const unpaginated = withoutNodeQueryMaterializedView(withoutNodeQueryPagination(descriptor));
|
|
1647
|
+
let candidates;
|
|
1648
|
+
if (this.storage.queryNodes && !this.nodeContentCipher) {
|
|
1649
|
+
candidates = (await this.storage.queryNodes(unpaginated)).nodes;
|
|
1650
|
+
} else {
|
|
1651
|
+
const fallback = await this.loadQueryFallbackCandidates(unpaginated);
|
|
1652
|
+
const decrypted = await Promise.all(
|
|
1653
|
+
fallback.nodes.map((node) => this.decryptNodeSnapshotIfPresent(node))
|
|
1654
|
+
);
|
|
1655
|
+
candidates = decrypted.filter((node) => node !== null);
|
|
1656
|
+
}
|
|
1657
|
+
if (overlay) {
|
|
1658
|
+
const present = new Set(candidates.map((node) => node.id));
|
|
1659
|
+
const missing = Object.keys(overlay.clones).filter(
|
|
1660
|
+
(id) => !present.has(id)
|
|
1661
|
+
);
|
|
1662
|
+
if (missing.length > 0) {
|
|
1663
|
+
const extra = await this.getNodesById(missing, this.storage);
|
|
1664
|
+
for (const node of extra.values()) {
|
|
1665
|
+
const decrypted = await this.decryptNodeSnapshotIfPresent(node);
|
|
1666
|
+
if (!decrypted) continue;
|
|
1667
|
+
if (decrypted.schemaId !== descriptor.schemaId) continue;
|
|
1668
|
+
if (decrypted.deleted && !descriptor.includeDeleted) continue;
|
|
1669
|
+
candidates.push(decrypted);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const readable = await this.filterReadableNodes(candidates);
|
|
1674
|
+
const swapped = await this.overlayStates(readable);
|
|
1675
|
+
const filtered = applyNodeQueryDescriptor(swapped, withoutNodeQueryPagination(descriptor));
|
|
1676
|
+
const result = applyNodeQueryDescriptor(swapped, descriptor);
|
|
1677
|
+
return {
|
|
1678
|
+
nodes: result,
|
|
1679
|
+
totalCount: filtered.length,
|
|
1680
|
+
plan: {
|
|
1681
|
+
strategy: "draft-overlay",
|
|
1682
|
+
candidateNodeCount: readable.length,
|
|
1683
|
+
hydratedNodeCount: swapped.length,
|
|
1684
|
+
returnedNodeCount: result.length,
|
|
1685
|
+
durationMs: Date.now() - start
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Resolve where a write to `id` lands under the checkout: an existing
|
|
1691
|
+
* clone, a lazily-forked one (`onMissingMember`), or — for non-members,
|
|
1692
|
+
* clone ids, and declined forks — the id itself.
|
|
1693
|
+
*/
|
|
1694
|
+
async resolveDraftWriteTarget(id) {
|
|
1695
|
+
const overlay = this.checkedOutDraft;
|
|
1696
|
+
if (!overlay) return id;
|
|
1697
|
+
const existing = overlay.clones[id];
|
|
1698
|
+
if (existing) return existing;
|
|
1699
|
+
if (this.draftDynamicMembers) {
|
|
1700
|
+
if (id === overlay.draftId || this.cloneToOriginal.has(id)) return id;
|
|
1701
|
+
} else if (!this.draftMemberSet.has(id)) {
|
|
1702
|
+
return id;
|
|
1703
|
+
}
|
|
1704
|
+
if (overlay.onMissingMember) {
|
|
1705
|
+
const cloneId = await overlay.onMissingMember(id);
|
|
1706
|
+
if (cloneId && this.checkedOutDraft === overlay) {
|
|
1707
|
+
this.checkedOutDraft = { ...overlay, clones: { ...overlay.clones, [id]: cloneId } };
|
|
1708
|
+
this.cloneToOriginal.set(cloneId, id);
|
|
1709
|
+
this.notifyDraftOverlayListeners();
|
|
1710
|
+
return cloneId;
|
|
1711
|
+
}
|
|
1712
|
+
if (cloneId) return cloneId;
|
|
1713
|
+
}
|
|
1714
|
+
return id;
|
|
1715
|
+
}
|
|
1527
1716
|
/**
|
|
1528
1717
|
* Return the subset of IDs that already exist in materialized storage.
|
|
1529
1718
|
*
|
|
@@ -1603,6 +1792,7 @@ var NodeStore = class {
|
|
|
1603
1792
|
*/
|
|
1604
1793
|
async update(id, options) {
|
|
1605
1794
|
const start = this.telemetry ? Date.now() : 0;
|
|
1795
|
+
id = await this.resolveDraftWriteTarget(id);
|
|
1606
1796
|
try {
|
|
1607
1797
|
if (this.canUseSingleWriteFastPath()) {
|
|
1608
1798
|
const { node: node2, previousNode, change: change2 } = await this.applySingleNodeWrite({
|
|
@@ -1611,7 +1801,7 @@ var NodeStore = class {
|
|
|
1611
1801
|
nodeId: id,
|
|
1612
1802
|
properties: options.properties
|
|
1613
1803
|
},
|
|
1614
|
-
authAction: "
|
|
1804
|
+
authAction: "update",
|
|
1615
1805
|
requireExisting: true
|
|
1616
1806
|
});
|
|
1617
1807
|
this.emit(change2, node2, previousNode, false);
|
|
@@ -1635,7 +1825,7 @@ var NodeStore = class {
|
|
|
1635
1825
|
}
|
|
1636
1826
|
await this.assertAuthorized({
|
|
1637
1827
|
subject: this.authorDID,
|
|
1638
|
-
action: "
|
|
1828
|
+
action: "update",
|
|
1639
1829
|
nodeId: id,
|
|
1640
1830
|
patch: options.properties,
|
|
1641
1831
|
node: {
|
|
@@ -1685,6 +1875,7 @@ var NodeStore = class {
|
|
|
1685
1875
|
*/
|
|
1686
1876
|
async delete(id) {
|
|
1687
1877
|
const start = this.telemetry ? Date.now() : 0;
|
|
1878
|
+
id = await this.resolveDraftWriteTarget(id);
|
|
1688
1879
|
try {
|
|
1689
1880
|
if (this.canUseSingleWriteFastPath()) {
|
|
1690
1881
|
const { node, previousNode, change: change2 } = await this.applySingleNodeWrite({
|
|
@@ -1750,6 +1941,7 @@ var NodeStore = class {
|
|
|
1750
1941
|
* Restore a deleted Node.
|
|
1751
1942
|
*/
|
|
1752
1943
|
async restore(id) {
|
|
1944
|
+
id = await this.resolveDraftWriteTarget(id);
|
|
1753
1945
|
if (this.canUseSingleWriteFastPath()) {
|
|
1754
1946
|
const { node: node2, previousNode, change: change2 } = await this.applySingleNodeWrite({
|
|
1755
1947
|
nodeId: id,
|
|
@@ -1758,7 +1950,7 @@ var NodeStore = class {
|
|
|
1758
1950
|
properties: {},
|
|
1759
1951
|
deleted: false
|
|
1760
1952
|
},
|
|
1761
|
-
authAction: "
|
|
1953
|
+
authAction: "update",
|
|
1762
1954
|
requireExisting: true
|
|
1763
1955
|
});
|
|
1764
1956
|
this.emit(change2, node2, previousNode, false);
|
|
@@ -1771,7 +1963,7 @@ var NodeStore = class {
|
|
|
1771
1963
|
}
|
|
1772
1964
|
await this.assertAuthorized({
|
|
1773
1965
|
subject: this.authorDID,
|
|
1774
|
-
action: "
|
|
1966
|
+
action: "update",
|
|
1775
1967
|
nodeId: id,
|
|
1776
1968
|
node: {
|
|
1777
1969
|
schemaId: existing.schemaId,
|
|
@@ -1818,7 +2010,8 @@ var NodeStore = class {
|
|
|
1818
2010
|
const readable = await this.filterReadableNodes(
|
|
1819
2011
|
decrypted.filter((node) => node !== null)
|
|
1820
2012
|
);
|
|
1821
|
-
const
|
|
2013
|
+
const paged = this.authEvaluator ? this.applyListPagination(readable, options) : readable;
|
|
2014
|
+
const result = await this.overlayStates(paged);
|
|
1822
2015
|
if (this.telemetry) {
|
|
1823
2016
|
this.telemetry.reportPerformance("data.list", Date.now() - start);
|
|
1824
2017
|
this.telemetry.reportUsage("data.list", 1);
|
|
@@ -1837,10 +2030,13 @@ var NodeStore = class {
|
|
|
1837
2030
|
async query(descriptor) {
|
|
1838
2031
|
const start = Date.now();
|
|
1839
2032
|
try {
|
|
2033
|
+
if (this.checkedOutDraft && Object.keys(this.checkedOutDraft.clones).length > 0 && (this.draftMemberSchemaSet === null || this.draftMemberSchemaSet.has(descriptor.schemaId))) {
|
|
2034
|
+
return this.draftAwareQuery(descriptor, start);
|
|
2035
|
+
}
|
|
1840
2036
|
if (this.storage.queryNodes && !this.nodeContentCipher && !this.authEvaluator) {
|
|
1841
2037
|
const result2 = await this.storage.queryNodes(descriptor);
|
|
1842
2038
|
return {
|
|
1843
|
-
nodes: result2.nodes,
|
|
2039
|
+
nodes: await this.overlayStates(result2.nodes),
|
|
1844
2040
|
totalCount: result2.totalCount,
|
|
1845
2041
|
plan: {
|
|
1846
2042
|
...result2.plan,
|
|
@@ -1852,7 +2048,7 @@ var NodeStore = class {
|
|
|
1852
2048
|
const authFingerprint = await this.authFingerprint();
|
|
1853
2049
|
const result2 = await this.storage.queryNodes({ ...descriptor, authFingerprint });
|
|
1854
2050
|
return {
|
|
1855
|
-
nodes: result2.nodes,
|
|
2051
|
+
nodes: await this.overlayStates(result2.nodes),
|
|
1856
2052
|
totalCount: result2.totalCount,
|
|
1857
2053
|
plan: {
|
|
1858
2054
|
...result2.plan,
|
|
@@ -1863,7 +2059,7 @@ var NodeStore = class {
|
|
|
1863
2059
|
if (this.storage.queryNodes && !this.nodeContentCipher && this.authEvaluator && !descriptor.nodeId && descriptor.materializedView === void 0) {
|
|
1864
2060
|
const candidates = await this.storage.queryNodes(withoutNodeQueryPagination(descriptor));
|
|
1865
2061
|
const readable2 = await this.filterReadableNodes(candidates.nodes);
|
|
1866
|
-
const result2 = applyNodeQueryDescriptor(readable2, descriptor);
|
|
2062
|
+
const result2 = await this.overlayStates(applyNodeQueryDescriptor(readable2, descriptor));
|
|
1867
2063
|
return {
|
|
1868
2064
|
nodes: result2,
|
|
1869
2065
|
totalCount: readable2.length,
|
|
@@ -1887,7 +2083,9 @@ var NodeStore = class {
|
|
|
1887
2083
|
const readable = await this.filterReadableNodes(
|
|
1888
2084
|
decrypted.filter((node) => node !== null)
|
|
1889
2085
|
);
|
|
1890
|
-
const result =
|
|
2086
|
+
const result = await this.overlayStates(
|
|
2087
|
+
applyNodeQueryDescriptor(readable, fallback.postFilterDescriptor)
|
|
2088
|
+
);
|
|
1891
2089
|
const totalCount = fallback.totalCount ?? (fallback.paginatedInStorage ? void 0 : applyNodeQueryDescriptor(
|
|
1892
2090
|
readable,
|
|
1893
2091
|
withoutNodeQueryPagination(fallback.postFilterDescriptor)
|
|
@@ -2472,12 +2670,21 @@ var NodeStore = class {
|
|
|
2472
2670
|
);
|
|
2473
2671
|
}
|
|
2474
2672
|
if (this.authEvaluator) {
|
|
2475
|
-
const
|
|
2673
|
+
const stored = await this.storage.getNode(change.payload.nodeId);
|
|
2674
|
+
const action = this.inferActionFromChange(change, stored);
|
|
2476
2675
|
const decision = await this.authEvaluator.can({
|
|
2477
2676
|
subject: change.authorDID,
|
|
2478
2677
|
action,
|
|
2479
2678
|
nodeId: change.payload.nodeId,
|
|
2480
|
-
patch: action === "
|
|
2679
|
+
patch: action === "create" || action === "update" ? change.payload.properties : void 0,
|
|
2680
|
+
// A remote create has no stored node to evaluate against — build the
|
|
2681
|
+
// draft from the payload so container-relation roles resolve
|
|
2682
|
+
// (exploration 0304). Update/delete load the stored node as before.
|
|
2683
|
+
node: action === "create" && change.payload.schemaId ? {
|
|
2684
|
+
schemaId: change.payload.schemaId,
|
|
2685
|
+
createdBy: change.authorDID,
|
|
2686
|
+
properties: change.payload.properties
|
|
2687
|
+
} : void 0
|
|
2481
2688
|
});
|
|
2482
2689
|
if (!decision.allowed) {
|
|
2483
2690
|
this.telemetry?.reportSecurityEvent("data.unauthorized_remote_change", "medium");
|
|
@@ -2673,6 +2880,24 @@ var NodeStore = class {
|
|
|
2673
2880
|
}
|
|
2674
2881
|
}
|
|
2675
2882
|
}
|
|
2883
|
+
const originalId = this.cloneToOriginal.get(change.payload.nodeId);
|
|
2884
|
+
if (originalId) {
|
|
2885
|
+
const originalListeners = this.nodeListeners.get(originalId);
|
|
2886
|
+
if (originalListeners) {
|
|
2887
|
+
const mirrored = {
|
|
2888
|
+
...event,
|
|
2889
|
+
node: event.node ? { ...event.node, id: originalId } : event.node,
|
|
2890
|
+
previousNode: event.previousNode ? { ...event.previousNode, id: originalId } : event.previousNode
|
|
2891
|
+
};
|
|
2892
|
+
for (const listener of originalListeners) {
|
|
2893
|
+
try {
|
|
2894
|
+
listener(mirrored);
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
console.error("Error in NodeStore draft-mirror listener:", err);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2676
2901
|
}
|
|
2677
2902
|
// ==========================================================================
|
|
2678
2903
|
// Internal Methods
|
|
@@ -2922,7 +3147,11 @@ var NodeStore = class {
|
|
|
2922
3147
|
const newTs = {
|
|
2923
3148
|
lamport: change.lamport,
|
|
2924
3149
|
author: change.authorDID,
|
|
2925
|
-
wallTime: change.wallTime
|
|
3150
|
+
wallTime: change.wallTime,
|
|
3151
|
+
// Grinding-resistant final tiebreak (exploration 0305): only v4+ changes
|
|
3152
|
+
// carry a key, so a v4-vs-legacy comparison degrades to the author DID
|
|
3153
|
+
// and mixed fleets still agree.
|
|
3154
|
+
...(change.protocolVersion ?? 0) >= LWW_TIEBREAK_KEY_VERSION ? { tiebreakKey: computeLwwTiebreakKey(change.authorDID, key, value) } : {}
|
|
2926
3155
|
};
|
|
2927
3156
|
const incomingWins = !existingTs || this.shouldReplace(existingTs, newTs);
|
|
2928
3157
|
const target = isUnknownProperty ? node._unknown ??= {} : node.properties;
|
|
@@ -3057,7 +3286,7 @@ var NodeStore = class {
|
|
|
3057
3286
|
if (operation.type === "create") {
|
|
3058
3287
|
await this.assertAuthorized({
|
|
3059
3288
|
subject: this.authorDID,
|
|
3060
|
-
action: "
|
|
3289
|
+
action: "create",
|
|
3061
3290
|
nodeId: operation.options.id ?? "",
|
|
3062
3291
|
node: {
|
|
3063
3292
|
schemaId: operation.options.schemaId,
|
|
@@ -3077,17 +3306,20 @@ var NodeStore = class {
|
|
|
3077
3306
|
}
|
|
3078
3307
|
await this.assertAuthorized({
|
|
3079
3308
|
subject: this.authorDID,
|
|
3080
|
-
action: "
|
|
3309
|
+
action: "update",
|
|
3081
3310
|
nodeId: operation.nodeId,
|
|
3082
3311
|
patch: operation.type === "update" ? operation.options.properties : void 0
|
|
3083
3312
|
});
|
|
3084
3313
|
}
|
|
3085
3314
|
}
|
|
3086
|
-
inferActionFromChange(change) {
|
|
3315
|
+
inferActionFromChange(change, stored) {
|
|
3087
3316
|
if (change.payload.deleted === true) {
|
|
3088
3317
|
return "delete";
|
|
3089
3318
|
}
|
|
3090
|
-
|
|
3319
|
+
if (!stored && change.payload.schemaId) {
|
|
3320
|
+
return "create";
|
|
3321
|
+
}
|
|
3322
|
+
return "update";
|
|
3091
3323
|
}
|
|
3092
3324
|
shouldRecomputeRecipients(schemaId, patch) {
|
|
3093
3325
|
const authRelevant = this.authRelevantPropertyLookup?.(schemaId);
|
|
@@ -3396,6 +3628,41 @@ var MemoryNodeStorageAdapter = class {
|
|
|
3396
3628
|
this.yjsSnapshotStore = this.yjsSnapshotStore.filter((s) => s.nodeId !== nodeId);
|
|
3397
3629
|
}
|
|
3398
3630
|
// ==========================================================================
|
|
3631
|
+
// Pin Registry (exploration 0329)
|
|
3632
|
+
// ==========================================================================
|
|
3633
|
+
/** key -> ownerId -> entry */
|
|
3634
|
+
pinStore = /* @__PURE__ */ new Map();
|
|
3635
|
+
pins = {
|
|
3636
|
+
addPins: async (pins) => {
|
|
3637
|
+
for (const pin of pins) {
|
|
3638
|
+
let owners = this.pinStore.get(pin.key);
|
|
3639
|
+
if (!owners) {
|
|
3640
|
+
owners = /* @__PURE__ */ new Map();
|
|
3641
|
+
this.pinStore.set(pin.key, owners);
|
|
3642
|
+
}
|
|
3643
|
+
owners.set(pin.ownerId, { ...pin });
|
|
3644
|
+
}
|
|
3645
|
+
},
|
|
3646
|
+
removePinsByOwner: async (ownerId) => {
|
|
3647
|
+
for (const [key, owners] of this.pinStore) {
|
|
3648
|
+
owners.delete(ownerId);
|
|
3649
|
+
if (owners.size === 0) this.pinStore.delete(key);
|
|
3650
|
+
}
|
|
3651
|
+
},
|
|
3652
|
+
getPinnedKeysAmong: async (keys) => {
|
|
3653
|
+
const pinned = /* @__PURE__ */ new Set();
|
|
3654
|
+
for (const key of keys) {
|
|
3655
|
+
if (this.pinStore.has(key)) pinned.add(key);
|
|
3656
|
+
}
|
|
3657
|
+
return pinned;
|
|
3658
|
+
},
|
|
3659
|
+
countPins: async () => {
|
|
3660
|
+
let count2 = 0;
|
|
3661
|
+
for (const owners of this.pinStore.values()) count2 += owners.size;
|
|
3662
|
+
return count2;
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
// ==========================================================================
|
|
3399
3666
|
// Utility Methods (for testing)
|
|
3400
3667
|
// ==========================================================================
|
|
3401
3668
|
/**
|
|
@@ -3407,6 +3674,7 @@ var MemoryNodeStorageAdapter = class {
|
|
|
3407
3674
|
this.nodes.clear();
|
|
3408
3675
|
this.documentContentStore.clear();
|
|
3409
3676
|
this.yjsSnapshotStore = [];
|
|
3677
|
+
this.pinStore.clear();
|
|
3410
3678
|
this.lastLamportTime = 0;
|
|
3411
3679
|
this.syncCursors.clear();
|
|
3412
3680
|
}
|
|
@@ -3446,6 +3714,9 @@ var MemoryNodeStorageAdapter = class {
|
|
|
3446
3714
|
])
|
|
3447
3715
|
),
|
|
3448
3716
|
yjsSnapshotStore: structuredClone(this.yjsSnapshotStore),
|
|
3717
|
+
pinStore: new Map(
|
|
3718
|
+
Array.from(this.pinStore.entries(), ([key, owners]) => [key, new Map(owners)])
|
|
3719
|
+
),
|
|
3449
3720
|
lastLamportTime: this.lastLamportTime
|
|
3450
3721
|
};
|
|
3451
3722
|
}
|
|
@@ -3455,6 +3726,7 @@ var MemoryNodeStorageAdapter = class {
|
|
|
3455
3726
|
this.nodes = snapshot.nodes;
|
|
3456
3727
|
this.documentContentStore = snapshot.documentContentStore;
|
|
3457
3728
|
this.yjsSnapshotStore = snapshot.yjsSnapshotStore;
|
|
3729
|
+
this.pinStore = snapshot.pinStore;
|
|
3458
3730
|
this.lastLamportTime = snapshot.lastLamportTime;
|
|
3459
3731
|
}
|
|
3460
3732
|
};
|
|
@@ -3520,7 +3792,10 @@ function hydrateJoinedRows(rows) {
|
|
|
3520
3792
|
node.timestamps[row.property_key] = {
|
|
3521
3793
|
lamport: row.lamport_time ?? 0,
|
|
3522
3794
|
author: row.updated_by ?? "",
|
|
3523
|
-
wallTime: row.prop_updated_at ?? 0
|
|
3795
|
+
wallTime: row.prop_updated_at ?? 0,
|
|
3796
|
+
// Round-trip the stored key so the in-memory LWW comparison matches the
|
|
3797
|
+
// SQL guard exactly (0305); NULL legacy rows keep the author tiebreak.
|
|
3798
|
+
...row.tiebreak_key != null ? { tiebreakKey: row.tiebreak_key } : {}
|
|
3524
3799
|
};
|
|
3525
3800
|
if ((row.prop_updated_at ?? 0) >= node.updatedAt) {
|
|
3526
3801
|
node.updatedBy = row.updated_by ?? node.createdBy;
|
|
@@ -3540,7 +3815,10 @@ function hydrateAggregatedRows(rows) {
|
|
|
3540
3815
|
timestamps[key] = {
|
|
3541
3816
|
lamport: entry.l ?? 0,
|
|
3542
3817
|
author: entry.b ?? "",
|
|
3543
|
-
wallTime: entry.w ?? 0
|
|
3818
|
+
wallTime: entry.w ?? 0,
|
|
3819
|
+
// Round-trip the stored tiebreak key (0305) so aggregated hydrate
|
|
3820
|
+
// matches the SQL guard; NULL/absent → author tiebreak.
|
|
3821
|
+
...entry.k != null ? { tiebreakKey: entry.k } : {}
|
|
3544
3822
|
};
|
|
3545
3823
|
if ((entry.w ?? 0) >= row.updated_at) {
|
|
3546
3824
|
node.updatedBy = entry.b ?? row.created_by;
|
|
@@ -3571,6 +3849,7 @@ function buildHydrateChunkQuery(ids) {
|
|
|
3571
3849
|
p.lamport_time,
|
|
3572
3850
|
p.updated_by,
|
|
3573
3851
|
p.updated_at AS prop_updated_at,
|
|
3852
|
+
p.tiebreak_key,
|
|
3574
3853
|
wanted.ordinal
|
|
3575
3854
|
FROM wanted
|
|
3576
3855
|
JOIN nodes n ON n.id = wanted.id
|
|
@@ -3599,7 +3878,7 @@ function buildAggregatedHydrateChunkQuery(ids) {
|
|
|
3599
3878
|
FILTER (WHERE p.property_key IS NOT NULL) AS props_json,
|
|
3600
3879
|
json_group_object(
|
|
3601
3880
|
p.property_key,
|
|
3602
|
-
json_object('l', p.lamport_time, 'b', p.updated_by, 'w', p.updated_at)
|
|
3881
|
+
json_object('l', p.lamport_time, 'b', p.updated_by, 'w', p.updated_at, 'k', p.tiebreak_key)
|
|
3603
3882
|
) FILTER (WHERE p.property_key IS NOT NULL) AS meta_json
|
|
3604
3883
|
FROM wanted
|
|
3605
3884
|
JOIN nodes n ON n.id = wanted.id
|
|
@@ -4522,7 +4801,12 @@ var NODE_PROPERTIES_LWW_GUARD = lwwUpdateGuardSql({
|
|
|
4522
4801
|
table: "node_properties",
|
|
4523
4802
|
lamportColumn: "lamport_time",
|
|
4524
4803
|
wallTimeColumn: "updated_at",
|
|
4525
|
-
authorColumn: "updated_by"
|
|
4804
|
+
authorColumn: "updated_by",
|
|
4805
|
+
// Grinding-resistant final tiebreak (exploration 0305): larger key wins when
|
|
4806
|
+
// both rows carry one (v4+), else the author DID. The key is precomputed in
|
|
4807
|
+
// application code and stored, so SQL only compares the opaque hex — byte
|
|
4808
|
+
// identical to `shouldReplace` in ./store.ts, no user-defined function.
|
|
4809
|
+
tiebreakKeyColumn: "tiebreak_key"
|
|
4526
4810
|
});
|
|
4527
4811
|
var DEFAULT_ADAPTIVE_INDEXING = {
|
|
4528
4812
|
enabled: false,
|
|
@@ -4622,6 +4906,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4622
4906
|
nodeReadAuthorizer;
|
|
4623
4907
|
/** One-time guard: ensure the `auth_fingerprint` column exists on upgraded DBs. */
|
|
4624
4908
|
materializationColumnsReady = false;
|
|
4909
|
+
nodePropertyColumnsReady = null;
|
|
4625
4910
|
/**
|
|
4626
4911
|
* Descriptor→SQL compilation lives in `query-compiler.ts` (exploration
|
|
4627
4912
|
* 0276). Flags are read per-compile so the compiler never captures stale
|
|
@@ -4916,12 +5201,14 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4916
5201
|
}
|
|
4917
5202
|
// ─── Materialized State Operations ────────────────────────────────────────
|
|
4918
5203
|
async getNode(id) {
|
|
5204
|
+
await this.ensureNodePropertyColumns();
|
|
4919
5205
|
const nodes = await hydrateNodesByIds(this.db, [id], this.aggregatedHydration);
|
|
4920
5206
|
return nodes[0] ?? null;
|
|
4921
5207
|
}
|
|
4922
5208
|
async getNodes(ids) {
|
|
4923
5209
|
const uniqueIds = Array.from(new Set(ids));
|
|
4924
5210
|
if (uniqueIds.length === 0) return [];
|
|
5211
|
+
await this.ensureNodePropertyColumns();
|
|
4925
5212
|
return hydrateNodesByIds(this.db, uniqueIds, this.aggregatedHydration);
|
|
4926
5213
|
}
|
|
4927
5214
|
async getExistingNodeIds(ids) {
|
|
@@ -4966,6 +5253,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4966
5253
|
* Used by importNodes to avoid nested transactions.
|
|
4967
5254
|
*/
|
|
4968
5255
|
async _setNodeInternal(node, options) {
|
|
5256
|
+
await this.ensureNodePropertyColumns();
|
|
4969
5257
|
await this.db.run(
|
|
4970
5258
|
`INSERT INTO nodes (id, schema_id, created_at, updated_at, created_by, deleted_at)
|
|
4971
5259
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
@@ -4987,13 +5275,14 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4987
5275
|
const timestamp = node.timestamps[key];
|
|
4988
5276
|
if (!timestamp) continue;
|
|
4989
5277
|
await this.db.run(
|
|
4990
|
-
`INSERT INTO node_properties (node_id, property_key, value, lamport_time, updated_by, updated_at)
|
|
4991
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
5278
|
+
`INSERT INTO node_properties (node_id, property_key, value, lamport_time, updated_by, updated_at, tiebreak_key)
|
|
5279
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4992
5280
|
ON CONFLICT(node_id, property_key) DO UPDATE SET
|
|
4993
5281
|
value = excluded.value,
|
|
4994
5282
|
lamport_time = excluded.lamport_time,
|
|
4995
5283
|
updated_by = excluded.updated_by,
|
|
4996
|
-
updated_at = excluded.updated_at
|
|
5284
|
+
updated_at = excluded.updated_at,
|
|
5285
|
+
tiebreak_key = excluded.tiebreak_key
|
|
4997
5286
|
WHERE ${NODE_PROPERTIES_LWW_GUARD}`,
|
|
4998
5287
|
[
|
|
4999
5288
|
node.id,
|
|
@@ -5001,7 +5290,8 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5001
5290
|
this.serializeValue(value),
|
|
5002
5291
|
timestamp.lamport,
|
|
5003
5292
|
timestamp.author,
|
|
5004
|
-
timestamp.wallTime
|
|
5293
|
+
timestamp.wallTime,
|
|
5294
|
+
timestamp.tiebreakKey ?? null
|
|
5005
5295
|
]
|
|
5006
5296
|
);
|
|
5007
5297
|
}
|
|
@@ -5040,6 +5330,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5040
5330
|
* Fetches nodes and properties in one query for better performance with large datasets.
|
|
5041
5331
|
*/
|
|
5042
5332
|
async listNodesOptimized(options) {
|
|
5333
|
+
await this.ensureNodePropertyColumns();
|
|
5043
5334
|
let whereClause = "1=1";
|
|
5044
5335
|
const params = [];
|
|
5045
5336
|
if (options?.schemaId) {
|
|
@@ -5063,7 +5354,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5063
5354
|
)
|
|
5064
5355
|
SELECT
|
|
5065
5356
|
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,
|
|
5357
|
+
p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at, p.tiebreak_key,
|
|
5067
5358
|
NULL as ordinal
|
|
5068
5359
|
FROM limited_nodes ln
|
|
5069
5360
|
LEFT JOIN node_properties p ON ln.id = p.node_id
|
|
@@ -5075,7 +5366,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5075
5366
|
sql = `
|
|
5076
5367
|
SELECT
|
|
5077
5368
|
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,
|
|
5369
|
+
p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at, p.tiebreak_key,
|
|
5079
5370
|
NULL as ordinal
|
|
5080
5371
|
FROM nodes n
|
|
5081
5372
|
LEFT JOIN node_properties p ON n.id = p.node_id
|
|
@@ -5101,6 +5392,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5101
5392
|
}
|
|
5102
5393
|
async queryNodes(descriptor) {
|
|
5103
5394
|
const start = Date.now();
|
|
5395
|
+
await this.ensureNodePropertyColumns();
|
|
5104
5396
|
if (descriptor.materializedView) {
|
|
5105
5397
|
return this.queryMaterializedView(descriptor, start);
|
|
5106
5398
|
}
|
|
@@ -5291,6 +5583,47 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5291
5583
|
[nodeId, content, Date.now()]
|
|
5292
5584
|
);
|
|
5293
5585
|
}
|
|
5586
|
+
// ─── Pin Registry (exploration 0329) ──────────────────────────────────────
|
|
5587
|
+
pins = {
|
|
5588
|
+
addPins: async (pins) => {
|
|
5589
|
+
if (pins.length === 0) return;
|
|
5590
|
+
await this.enqueueWrite(async () => {
|
|
5591
|
+
const now = Date.now();
|
|
5592
|
+
for (const chunk of chunkItems(pins, 200)) {
|
|
5593
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
5594
|
+
const params = [];
|
|
5595
|
+
for (const pin of chunk) params.push(pin.key, pin.ownerId, pin.reason, now);
|
|
5596
|
+
await this.db.run(
|
|
5597
|
+
`INSERT INTO pinned_changes (pin_key, owner_id, reason, created_at)
|
|
5598
|
+
VALUES ${placeholders}
|
|
5599
|
+
ON CONFLICT(pin_key, owner_id) DO NOTHING`,
|
|
5600
|
+
params
|
|
5601
|
+
);
|
|
5602
|
+
}
|
|
5603
|
+
});
|
|
5604
|
+
},
|
|
5605
|
+
removePinsByOwner: async (ownerId) => {
|
|
5606
|
+
await this.enqueueWrite(async () => {
|
|
5607
|
+
await this.db.run(`DELETE FROM pinned_changes WHERE owner_id = ?`, [ownerId]);
|
|
5608
|
+
});
|
|
5609
|
+
},
|
|
5610
|
+
getPinnedKeysAmong: async (keys) => {
|
|
5611
|
+
const pinned = /* @__PURE__ */ new Set();
|
|
5612
|
+
for (const chunk of chunkItems(keys, 500)) {
|
|
5613
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
5614
|
+
const rows = await this.db.query(
|
|
5615
|
+
`SELECT DISTINCT pin_key FROM pinned_changes WHERE pin_key IN (${placeholders})`,
|
|
5616
|
+
[...chunk]
|
|
5617
|
+
);
|
|
5618
|
+
for (const row of rows) pinned.add(row.pin_key);
|
|
5619
|
+
}
|
|
5620
|
+
return pinned;
|
|
5621
|
+
},
|
|
5622
|
+
countPins: async () => {
|
|
5623
|
+
const row = await this.db.queryOne(`SELECT COUNT(*) AS n FROM pinned_changes`);
|
|
5624
|
+
return row?.n ?? 0;
|
|
5625
|
+
}
|
|
5626
|
+
};
|
|
5294
5627
|
// ─── Yjs Snapshots (Extended) ─────────────────────────────────────────────
|
|
5295
5628
|
/**
|
|
5296
5629
|
* Save a Yjs snapshot for time travel.
|
|
@@ -5588,6 +5921,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5588
5921
|
if (!this.db.transactionBatch) {
|
|
5589
5922
|
throw new Error("SQLite transactionBatch is not available.");
|
|
5590
5923
|
}
|
|
5924
|
+
await this.ensureNodePropertyColumns();
|
|
5591
5925
|
const operations = [];
|
|
5592
5926
|
const indexProperties = options?.indexProperties ?? true;
|
|
5593
5927
|
const hasFullTextSearch = await this.fullTextIndexing.hasTable();
|
|
@@ -5638,16 +5972,17 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5638
5972
|
const timestamp = node.timestamps[key];
|
|
5639
5973
|
if (!timestamp) continue;
|
|
5640
5974
|
operations.push({
|
|
5641
|
-
// Full LWW ordering
|
|
5642
|
-
// upsert above and `shouldReplace` in ./store.ts (0272).
|
|
5975
|
+
// Full LWW ordering chain — keep in lockstep with the setNode
|
|
5976
|
+
// upsert above and `shouldReplace` in ./store.ts (0272/0305).
|
|
5643
5977
|
sql: `INSERT INTO node_properties
|
|
5644
|
-
(node_id, property_key, value, lamport_time, updated_by, updated_at)
|
|
5645
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
5978
|
+
(node_id, property_key, value, lamport_time, updated_by, updated_at, tiebreak_key)
|
|
5979
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
5646
5980
|
ON CONFLICT(node_id, property_key) DO UPDATE SET
|
|
5647
5981
|
value = excluded.value,
|
|
5648
5982
|
lamport_time = excluded.lamport_time,
|
|
5649
5983
|
updated_by = excluded.updated_by,
|
|
5650
|
-
updated_at = excluded.updated_at
|
|
5984
|
+
updated_at = excluded.updated_at,
|
|
5985
|
+
tiebreak_key = excluded.tiebreak_key
|
|
5651
5986
|
WHERE ${NODE_PROPERTIES_LWW_GUARD}`,
|
|
5652
5987
|
params: [
|
|
5653
5988
|
node.id,
|
|
@@ -5655,7 +5990,8 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5655
5990
|
this.serializeValue(value),
|
|
5656
5991
|
timestamp.lamport,
|
|
5657
5992
|
timestamp.author,
|
|
5658
|
-
timestamp.wallTime
|
|
5993
|
+
timestamp.wallTime,
|
|
5994
|
+
timestamp.tiebreakKey ?? null
|
|
5659
5995
|
]
|
|
5660
5996
|
});
|
|
5661
5997
|
}
|
|
@@ -5907,6 +6243,25 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
5907
6243
|
}
|
|
5908
6244
|
this.materializationColumnsReady = true;
|
|
5909
6245
|
}
|
|
6246
|
+
/**
|
|
6247
|
+
* Ensure the `node_properties.tiebreak_key` column exists (exploration 0305).
|
|
6248
|
+
* Fresh databases get it from the DDL; databases created before schema v8 need
|
|
6249
|
+
* this in-place add so the grinding-resistant LWW guard has a column to
|
|
6250
|
+
* compare. Idempotent, memoized, and non-fatal on races — a legacy NULL key
|
|
6251
|
+
* just falls back to the author-DID tiebreak.
|
|
6252
|
+
*/
|
|
6253
|
+
async ensureNodePropertyColumns() {
|
|
6254
|
+
this.nodePropertyColumnsReady ??= (async () => {
|
|
6255
|
+
try {
|
|
6256
|
+
const columns = await this.db.query(`PRAGMA table_info(node_properties)`);
|
|
6257
|
+
if (!columns.some((column) => column.name === "tiebreak_key")) {
|
|
6258
|
+
await this.db.run("ALTER TABLE node_properties ADD COLUMN tiebreak_key TEXT");
|
|
6259
|
+
}
|
|
6260
|
+
} catch {
|
|
6261
|
+
}
|
|
6262
|
+
})();
|
|
6263
|
+
return this.nodePropertyColumnsReady;
|
|
6264
|
+
}
|
|
5910
6265
|
setNodeReadAuthorizer(authorizer) {
|
|
5911
6266
|
this.nodeReadAuthorizer = authorizer;
|
|
5912
6267
|
}
|