@harperfast/harper-pro 5.1.0-beta.3 → 5.1.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.
Files changed (59) hide show
  1. package/core/DESIGN.md +24 -0
  2. package/core/bin/copyDb.ts +16 -3
  3. package/core/package-lock.json +44 -54
  4. package/core/resources/Table.ts +41 -4
  5. package/core/resources/databases.ts +134 -42
  6. package/core/resources/graphql.ts +3 -0
  7. package/core/resources/replayLogs.ts +114 -42
  8. package/core/resources/replayLogsGuards.ts +58 -9
  9. package/core/server/nodeName.ts +9 -3
  10. package/core/server/operationsServer.ts +17 -0
  11. package/core/utility/install/installer.ts +12 -5
  12. package/dist/core/bin/copyDb.js +16 -3
  13. package/dist/core/bin/copyDb.js.map +1 -1
  14. package/dist/core/resources/Table.js +43 -4
  15. package/dist/core/resources/Table.js.map +1 -1
  16. package/dist/core/resources/databases.js +135 -41
  17. package/dist/core/resources/databases.js.map +1 -1
  18. package/dist/core/resources/graphql.js +3 -0
  19. package/dist/core/resources/graphql.js.map +1 -1
  20. package/dist/core/resources/replayLogs.js +100 -40
  21. package/dist/core/resources/replayLogs.js.map +1 -1
  22. package/dist/core/resources/replayLogsGuards.js +50 -10
  23. package/dist/core/resources/replayLogsGuards.js.map +1 -1
  24. package/dist/core/server/nodeName.js +9 -3
  25. package/dist/core/server/nodeName.js.map +1 -1
  26. package/dist/core/server/operationsServer.js +20 -0
  27. package/dist/core/server/operationsServer.js.map +1 -1
  28. package/dist/core/utility/install/installer.js +12 -6
  29. package/dist/core/utility/install/installer.js.map +1 -1
  30. package/dist/replication/knownNodes.js +106 -17
  31. package/dist/replication/knownNodes.js.map +1 -1
  32. package/dist/replication/replicator.js +6 -3
  33. package/dist/replication/replicator.js.map +1 -1
  34. package/npm-shrinkwrap.json +43 -52
  35. package/package.json +3 -3
  36. package/replication/knownNodes.ts +104 -14
  37. package/replication/replicator.ts +7 -3
  38. package/studio/web/assets/{Chat-BdFickL8.js → Chat-DMBW4pI2.js} +2 -2
  39. package/studio/web/assets/{Chat-BdFickL8.js.map → Chat-DMBW4pI2.js.map} +1 -1
  40. package/studio/web/assets/{FloatingChat-CaAoco8I.js → FloatingChat-DTmhPsrm.js} +4 -4
  41. package/studio/web/assets/{FloatingChat-CaAoco8I.js.map → FloatingChat-DTmhPsrm.js.map} +1 -1
  42. package/studio/web/assets/{applications-C3y3xwyG.js → applications-CigxJarn.js} +2 -2
  43. package/studio/web/assets/{applications-C3y3xwyG.js.map → applications-CigxJarn.js.map} +1 -1
  44. package/studio/web/assets/{index-Cd3Zh3nK.js → index-oRZw5GW3.js} +6 -6
  45. package/studio/web/assets/index-oRZw5GW3.js.map +1 -0
  46. package/studio/web/assets/{index.lazy-0hLbh5Fo.js → index.lazy-DY3VcR86.js} +4 -4
  47. package/studio/web/assets/{index.lazy-0hLbh5Fo.js.map → index.lazy-DY3VcR86.js.map} +1 -1
  48. package/studio/web/assets/{profile-CwBWGuVt.js → profile-DiV60L50.js} +2 -2
  49. package/studio/web/assets/{profile-CwBWGuVt.js.map → profile-DiV60L50.js.map} +1 -1
  50. package/studio/web/assets/{setComponentFile-Dhrmd8eR.js → setComponentFile-Bz6WI4jy.js} +2 -2
  51. package/studio/web/assets/{setComponentFile-Dhrmd8eR.js.map → setComponentFile-Bz6WI4jy.js.map} +1 -1
  52. package/studio/web/assets/{status-Bykq6QcD.js → status-fW6PBpPM.js} +2 -2
  53. package/studio/web/assets/{status-Bykq6QcD.js.map → status-fW6PBpPM.js.map} +1 -1
  54. package/studio/web/assets/{swagger-ui-react-CF94s29D.js → swagger-ui-react-DRYf7G3J.js} +2 -2
  55. package/studio/web/assets/{swagger-ui-react-CF94s29D.js.map → swagger-ui-react-DRYf7G3J.js.map} +1 -1
  56. package/studio/web/assets/{useEntityRestURL-D7vuaG2W.js → useEntityRestURL-14jWKu9J.js} +2 -2
  57. package/studio/web/assets/{useEntityRestURL-D7vuaG2W.js.map → useEntityRestURL-14jWKu9J.js.map} +1 -1
  58. package/studio/web/index.html +1 -1
  59. package/studio/web/assets/index-Cd3Zh3nK.js.map +0 -1
package/core/DESIGN.md CHANGED
@@ -107,3 +107,27 @@ Adding a new system table (e.g. `hdb_deployment` in #641 Slice A) requires three
107
107
  System tables replicate by default. To opt out, add the name to `NON_REPLICATING_SYSTEM_TABLES` in `resources/databases.ts`. The check happens after table init and sets `table.replicate = false` per-node.
108
108
 
109
109
  If the table needs `audit: true`, set it both in the schema (for fresh installs) **and** on the `CreateTableObject` instance in the directive (for upgrades) — otherwise the two paths diverge.
110
+
111
+ ## Table drops, the `dropping` tombstone, and ghost tables
112
+
113
+ A table is a set of RocksDB column families (`T/` plus `T/<attr>`) and a set of catalog rows
114
+ in the `__dbis__` store, with no transaction spanning the two. `Table.dropTable()` therefore
115
+ persists a `dropping: true` flag on the table's primary catalog entry (`T/`) before any
116
+ destructive work, then drops the column families (awaited - a failed drop must surface as the
117
+ operation's error, never a swallowed rejection), then removes the catalog rows. If the process
118
+ dies or a drop fails partway, the tombstone survives; both the boot-time schema load in
119
+ `databases.ts` (`completeInterruptedDrop`) and a same-name `table()` create complete the
120
+ interrupted drop instead of resurrecting the table. Without this, surviving catalog rows are
121
+ silently re-opened with create-if-missing on the next start, which resurrects "deleted" tables
122
+ (with their data, if the column families were never actually removed).
123
+
124
+ Two related traps: the create path's exclusive `update-attributes` lock is a synchronous spin
125
+ lock (`while (!tryLock()) {}`), so any throw inside the create window must release it or every
126
+ subsequent create on that database pins a worker at 100% CPU. And dropping then recreating a
127
+ same-named table within one process requires @harperfast/rocksdb-js >= the column-family
128
+ eviction fix (1.4.3 / rocksdb-js#<main PR>): older bindings keep the dropped column family's
129
+ by-name registry entry alive whenever other worker threads hold handles, so the recreate
130
+ silently reuses a dangling handle and every write fails with "Invalid column family specified
131
+ in write batch", poisoning the whole database env until restart. The regression suite for all
132
+ of this is `unitTests/resources/dropTableGhost.test.js` (it fails by design on pre-fix
133
+ bindings).
@@ -381,20 +381,33 @@ export async function copyDbToRocks(sourceRootStore, sourceDatabase: string, tar
381
381
  const sourceDbisDb = sourceRootStore.dbisDb;
382
382
 
383
383
  const targetRootStore = openRocksDb(targetPath, { disableWAL: false });
384
+ // sharedStructuresKey wires the rocksdb-js getStructures/saveStructures closures
385
+ // so that the plain msgpackr.Encoder used here persists structures within the
386
+ // __dbis__ CF at Symbol.for('structures'). The runtime attributesDbi RecordEncoder
387
+ // takes the non-isRocksDB path (handleLocalTimeForGets is never called on it) and
388
+ // reads from the same CF key via superGetStructures. Without this, own structure
389
+ // IDs starting at 0x40 are minted in-memory and silently lost on restart →
390
+ // runtime decoder interprets 0x40 as fixint 64 → "Data read, but end of buffer
391
+ // not reached 64" (harper#1260).
384
392
  const targetDbisDb = openRocksDb(targetPath, {
385
393
  disableWAL: false,
386
394
  name: INTERNAL_DBIS_NAME,
395
+ sharedStructuresKey: Symbol.for('structures'),
387
396
  });
388
397
 
389
398
  const STRUCTURES_KEY = Symbol.for('structures');
390
- const copyStructures = (sourceDbi, storeName: string) => {
399
+ const copyStructures = (sourceDbi, storeName: string, extraTarget?: RocksDatabase) => {
391
400
  const buffer = sourceDbi.getBinary?.(STRUCTURES_KEY);
392
401
  if (buffer) {
393
- targetRootStore.putSync([STRUCTURES_KEY, storeName], asBinary(buffer));
402
+ const binaryBuffer = asBinary(buffer);
403
+ targetRootStore.putSync([STRUCTURES_KEY, storeName], binaryBuffer);
404
+ // Also write to the extra target CF when provided (e.g. __dbis__ CF,
405
+ // which the runtime RecordEncoder reads via its superGetStructures path).
406
+ extraTarget?.putSync(STRUCTURES_KEY, binaryBuffer);
394
407
  }
395
408
  };
396
409
 
397
- copyStructures(sourceDbisDb, INTERNAL_DBIS_NAME);
410
+ copyStructures(sourceDbisDb, INTERNAL_DBIS_NAME, targetDbisDb);
398
411
 
399
412
  let written;
400
413
  let outstandingWrites = 0;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "harper",
3
- "version": "5.1.0-beta.2",
3
+ "version": "5.1.0-beta.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "harper",
9
- "version": "5.1.0-beta.2",
9
+ "version": "5.1.0-beta.3",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@aws-sdk/client-s3": "^3.1012.0",
@@ -17,7 +17,7 @@
17
17
  "@fastify/cors": "^11.2.0",
18
18
  "@fastify/static": "^9.1.3",
19
19
  "@harperfast/extended-iterable": "^1.0.1",
20
- "@harperfast/rocksdb-js": "^2.0.0",
20
+ "@harperfast/rocksdb-js": "^2.1.0",
21
21
  "@turf/area": "6.5.0",
22
22
  "@turf/boolean-contains": "6.5.0",
23
23
  "@turf/boolean-disjoint": "6.5.0",
@@ -95,7 +95,7 @@
95
95
  },
96
96
  "devDependencies": {
97
97
  "@harperdb/code-guidelines": "^0.0.6",
98
- "@harperfast/integration-testing": "^0.3.1",
98
+ "@harperfast/integration-testing": "^0.5.1",
99
99
  "@modelcontextprotocol/sdk": "^1.29.0",
100
100
  "@types/busboy": "^1.5.4",
101
101
  "@types/fs-extra": "^11.0.4",
@@ -130,8 +130,7 @@
130
130
  "why-is-node-still-running": "^1.0.0"
131
131
  },
132
132
  "engines": {
133
- "minimum-node": "20.0.0",
134
- "node": ">=20"
133
+ "node": "^22.18.0 || >=24.0.0"
135
134
  },
136
135
  "optionalDependencies": {
137
136
  "bufferutil": "^4.0.9",
@@ -2898,9 +2897,9 @@
2898
2897
  "license": "Apache-2.0"
2899
2898
  },
2900
2899
  "node_modules/@harperfast/integration-testing": {
2901
- "version": "0.3.1",
2902
- "resolved": "https://registry.npmjs.org/@harperfast/integration-testing/-/integration-testing-0.3.1.tgz",
2903
- "integrity": "sha512-hW7XsSTRWv38pK0nY4GZhGmmWAeQg/2eSSHAdwOO+niL7QORLExGjKCYxySylpKbWRdORQ7JjG5RMkFH+LQc9g==",
2900
+ "version": "0.5.1",
2901
+ "resolved": "https://registry.npmjs.org/@harperfast/integration-testing/-/integration-testing-0.5.1.tgz",
2902
+ "integrity": "sha512-hHkDf9mgxdKFPqnlc1Dtz9je9WafUWGIofyzgfpmPWECxWqEo4NIa5CesJANtdKHSVsUtSiEt0E05y6+ZsBtPw==",
2904
2903
  "dev": true,
2905
2904
  "license": "Apache-2.0",
2906
2905
  "dependencies": {
@@ -2923,13 +2922,13 @@
2923
2922
  }
2924
2923
  },
2925
2924
  "node_modules/@harperfast/rocksdb-js": {
2926
- "version": "2.0.0",
2927
- "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js/-/rocksdb-js-2.0.0.tgz",
2928
- "integrity": "sha512-70ibZ4bg1kWaGnftsI/yRxPMzdwgLNJcryz3NtiZTywtlwpqcd9Q3wedXbQooayE7PYJmDJXIRDQj3KYkvud9w==",
2925
+ "version": "2.1.0",
2926
+ "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js/-/rocksdb-js-2.1.0.tgz",
2927
+ "integrity": "sha512-1E6Yiu7U0HHVPzJQE3t3ifwyfqXjmmhaoBahN6JWzswu6TMkY3EpCNDozpZ2MVom/4b/7sUG9IRKUpX8h9XK/Q==",
2929
2928
  "license": "Apache-2.0",
2930
2929
  "dependencies": {
2931
2930
  "@harperfast/extended-iterable": "1.0.3",
2932
- "msgpackr": "2.0.2",
2931
+ "msgpackr": "2.0.4",
2933
2932
  "ordered-binary": "1.6.1"
2934
2933
  },
2935
2934
  "bin": {
@@ -2939,20 +2938,20 @@
2939
2938
  "node": "^22.18.0 || >=24.0.0"
2940
2939
  },
2941
2940
  "optionalDependencies": {
2942
- "@harperfast/rocksdb-js-darwin-arm64": "2.0.0",
2943
- "@harperfast/rocksdb-js-darwin-x64": "2.0.0",
2944
- "@harperfast/rocksdb-js-linux-arm64-glibc": "2.0.0",
2945
- "@harperfast/rocksdb-js-linux-arm64-musl": "2.0.0",
2946
- "@harperfast/rocksdb-js-linux-x64-glibc": "2.0.0",
2947
- "@harperfast/rocksdb-js-linux-x64-musl": "2.0.0",
2948
- "@harperfast/rocksdb-js-win32-arm64": "2.0.0",
2949
- "@harperfast/rocksdb-js-win32-x64": "2.0.0"
2941
+ "@harperfast/rocksdb-js-darwin-arm64": "2.1.0",
2942
+ "@harperfast/rocksdb-js-darwin-x64": "2.1.0",
2943
+ "@harperfast/rocksdb-js-linux-arm64-glibc": "2.1.0",
2944
+ "@harperfast/rocksdb-js-linux-arm64-musl": "2.1.0",
2945
+ "@harperfast/rocksdb-js-linux-x64-glibc": "2.1.0",
2946
+ "@harperfast/rocksdb-js-linux-x64-musl": "2.1.0",
2947
+ "@harperfast/rocksdb-js-win32-arm64": "2.1.0",
2948
+ "@harperfast/rocksdb-js-win32-x64": "2.1.0"
2950
2949
  }
2951
2950
  },
2952
2951
  "node_modules/@harperfast/rocksdb-js-darwin-arm64": {
2953
- "version": "2.0.0",
2954
- "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-darwin-arm64/-/rocksdb-js-darwin-arm64-2.0.0.tgz",
2955
- "integrity": "sha512-1Ed3XVekAFdoJJfDsrjBYJT1B85Zj91gQcC7fQq/CnhRdEk/D1ZdCsg7q9cg++mpZ5Ksnx+BsfiyqO67bTmKpQ==",
2952
+ "version": "2.1.0",
2953
+ "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-darwin-arm64/-/rocksdb-js-darwin-arm64-2.1.0.tgz",
2954
+ "integrity": "sha512-KATHEEcKv1sBsuqaxgAd5ZqRNN63f/dedl5ilE0aZKubDjPHJFWLl1USZZXd4FMR7bYHzCV823S9RxZII1UTjA==",
2956
2955
  "cpu": [
2957
2956
  "arm64"
2958
2957
  ],
@@ -2966,9 +2965,9 @@
2966
2965
  }
2967
2966
  },
2968
2967
  "node_modules/@harperfast/rocksdb-js-darwin-x64": {
2969
- "version": "2.0.0",
2970
- "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-darwin-x64/-/rocksdb-js-darwin-x64-2.0.0.tgz",
2971
- "integrity": "sha512-dK85CtR9RXhN5GF7Ndpt7krO5zImAl5pGyrx3QmepI72uT8ncppYurDEPZZ0z/A3LrBidXbaznUp2iGFVdYUYg==",
2968
+ "version": "2.1.0",
2969
+ "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-darwin-x64/-/rocksdb-js-darwin-x64-2.1.0.tgz",
2970
+ "integrity": "sha512-8PgipGp9uviZ+c8amA1MiKthekL4Qxmd/wPGbaa2P5MEj7cW3nXwuwuLrXFtj0plfbGJZDKHAoHH1MQc63JiHQ==",
2972
2971
  "cpu": [
2973
2972
  "x64"
2974
2973
  ],
@@ -2982,9 +2981,9 @@
2982
2981
  }
2983
2982
  },
2984
2983
  "node_modules/@harperfast/rocksdb-js-linux-arm64-glibc": {
2985
- "version": "2.0.0",
2986
- "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-linux-arm64-glibc/-/rocksdb-js-linux-arm64-glibc-2.0.0.tgz",
2987
- "integrity": "sha512-au7qK8VCkc6uU2lp2cRcO7HLML0ehfKFBimFYnxdwIHo6wZ1lPW5YVxKnuKIBKThMxdV6Dn2XJ3pu3ub8KtPrA==",
2984
+ "version": "2.1.0",
2985
+ "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-linux-arm64-glibc/-/rocksdb-js-linux-arm64-glibc-2.1.0.tgz",
2986
+ "integrity": "sha512-kzlN++3RaECDeKLYP7a3A/7D1IrTu8DzVRdvQ90rjNi67ZhiTfPGLwkEJgSheGRaKOkvtXOcqaG+G0uLCmY2qg==",
2988
2987
  "cpu": [
2989
2988
  "arm64"
2990
2989
  ],
@@ -3001,9 +3000,9 @@
3001
3000
  }
3002
3001
  },
3003
3002
  "node_modules/@harperfast/rocksdb-js-linux-arm64-musl": {
3004
- "version": "2.0.0",
3005
- "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-linux-arm64-musl/-/rocksdb-js-linux-arm64-musl-2.0.0.tgz",
3006
- "integrity": "sha512-x3F2h7fVwH1IHYlQDFOO5YJ9NH3pPkZybjPOaV1Vo8BiE9A72BabcVbQeV98/3STcMP2k20YpoggsI8QjcdCzQ==",
3003
+ "version": "2.1.0",
3004
+ "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-linux-arm64-musl/-/rocksdb-js-linux-arm64-musl-2.1.0.tgz",
3005
+ "integrity": "sha512-Wm3GCuII2+2wUdpF14qMJpFBvYMS5LCNmqYqc8PLQdM9+1C0pp2xp/YlXz295lSJwDRBGpZQ4tT/fCIhQb3RZg==",
3007
3006
  "cpu": [
3008
3007
  "arm64"
3009
3008
  ],
@@ -3020,9 +3019,9 @@
3020
3019
  }
3021
3020
  },
3022
3021
  "node_modules/@harperfast/rocksdb-js-linux-x64-glibc": {
3023
- "version": "2.0.0",
3024
- "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-linux-x64-glibc/-/rocksdb-js-linux-x64-glibc-2.0.0.tgz",
3025
- "integrity": "sha512-RDinS/7loqA7WuKQvfB73yc5dveTrOpdP/pXlFepWG1P586/0kpp0abRgCpkuSZyhmqIoWX9n9ooZvh03pvZ3A==",
3022
+ "version": "2.1.0",
3023
+ "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-linux-x64-glibc/-/rocksdb-js-linux-x64-glibc-2.1.0.tgz",
3024
+ "integrity": "sha512-JOR19bhySF+fwFoxsR1JOURSxUySL1NE6wLBtWfLSA4SEQZXi/Xzkkq7Up92JuL9E4ATcEkVJjPy52azUUE4VA==",
3026
3025
  "cpu": [
3027
3026
  "x64"
3028
3027
  ],
@@ -3039,9 +3038,9 @@
3039
3038
  }
3040
3039
  },
3041
3040
  "node_modules/@harperfast/rocksdb-js-linux-x64-musl": {
3042
- "version": "2.0.0",
3043
- "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-linux-x64-musl/-/rocksdb-js-linux-x64-musl-2.0.0.tgz",
3044
- "integrity": "sha512-gswLCyzqPUoB2HlTE+DMOd2Nr7H9uUg/pgtZpIvTpHU6ewV2O8mANT6qV/DLN9uPKWrBSp77b92PxXYtPRyPsA==",
3041
+ "version": "2.1.0",
3042
+ "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-linux-x64-musl/-/rocksdb-js-linux-x64-musl-2.1.0.tgz",
3043
+ "integrity": "sha512-HhUeWFqlEdjDPQecO1Exp3zm4LmXV2biyWPycXWBwXpitaI3YbDaUkiZHLT5QmGdYMHQou9s9RrcJ0B0xBpaYA==",
3045
3044
  "cpu": [
3046
3045
  "x64"
3047
3046
  ],
@@ -3058,9 +3057,9 @@
3058
3057
  }
3059
3058
  },
3060
3059
  "node_modules/@harperfast/rocksdb-js-win32-arm64": {
3061
- "version": "2.0.0",
3062
- "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-win32-arm64/-/rocksdb-js-win32-arm64-2.0.0.tgz",
3063
- "integrity": "sha512-vwVMVS/Nw5Mw17whMVQG7eJfBXqS2NeABiCxgsd2SoV6ovvm0TbSg9D8z2hsJlSJGy8BSYxK8i+xgqf1oh1ogA==",
3060
+ "version": "2.1.0",
3061
+ "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-win32-arm64/-/rocksdb-js-win32-arm64-2.1.0.tgz",
3062
+ "integrity": "sha512-pUxyRGlyx+o0zU/ioUb4Z7ZQPy01iDU0gr7YQnOn9gCyDo1ibeeZVKDYZJ+WEpQCWEdea5YJVrSt597BQPAJiQ==",
3064
3063
  "cpu": [
3065
3064
  "arm64"
3066
3065
  ],
@@ -3074,9 +3073,9 @@
3074
3073
  }
3075
3074
  },
3076
3075
  "node_modules/@harperfast/rocksdb-js-win32-x64": {
3077
- "version": "2.0.0",
3078
- "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-win32-x64/-/rocksdb-js-win32-x64-2.0.0.tgz",
3079
- "integrity": "sha512-WzaatIsLBjWrmRHHCcOeQ3TqUpBiEoaKBI6SbG6UQRO41gI7Ts0LVHtYF195Luldc8XsibSic1VztEPh2ubVAQ==",
3076
+ "version": "2.1.0",
3077
+ "resolved": "https://registry.npmjs.org/@harperfast/rocksdb-js-win32-x64/-/rocksdb-js-win32-x64-2.1.0.tgz",
3078
+ "integrity": "sha512-C0XFhYIUAJxYKUV+P12lgzEhsH7NMzlSBq8WtEQun8PLI2WJIWpeo/UI7Z/YLKcO8dR8mYh1w/uui/vBC86sRg==",
3080
3079
  "cpu": [
3081
3080
  "x64"
3082
3081
  ],
@@ -3089,15 +3088,6 @@
3089
3088
  "node": "^22.18.0 || >=24.0.0"
3090
3089
  }
3091
3090
  },
3092
- "node_modules/@harperfast/rocksdb-js/node_modules/msgpackr": {
3093
- "version": "2.0.2",
3094
- "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.2.tgz",
3095
- "integrity": "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ==",
3096
- "license": "MIT",
3097
- "optionalDependencies": {
3098
- "msgpackr-extract": "^3.0.4"
3099
- }
3100
- },
3101
3091
  "node_modules/@hono/node-server": {
3102
3092
  "version": "1.19.14",
3103
3093
  "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
@@ -1017,6 +1017,29 @@ export function makeTable(options) {
1017
1017
  }
1018
1018
 
1019
1019
  static async dropTable() {
1020
+ if (databaseName === databasePath) {
1021
+ // Persist a drop tombstone on the primary catalog entry BEFORE any
1022
+ // destructive work. If the process dies or a column family drop fails
1023
+ // partway through, the tombstone survives with the catalog rows, and
1024
+ // the next startup (or a same-name create) completes the drop via
1025
+ // completeInterruptedDrop in databases.ts instead of resurrecting
1026
+ // the table.
1027
+ const primaryCatalogKey = TableResource.tableName + '/';
1028
+ const primaryMeta = (dbisDb as any).getSync(primaryCatalogKey);
1029
+ if (primaryMeta && !primaryMeta.dropping) {
1030
+ primaryMeta.dropping = true;
1031
+ // put is rebound to putSync on RocksDB stores; on LMDB it returns
1032
+ // a promise, so await it to make the tombstone durable before the
1033
+ // destructive work below
1034
+ const tombstoneWrite = (dbisDb as any).put(primaryCatalogKey, primaryMeta);
1035
+ if (tombstoneWrite?.then) await tombstoneWrite;
1036
+ }
1037
+ }
1038
+ // Remove the table from the in-memory schema immediately so concurrent
1039
+ // requests get "table does not exist" instead of racing the column
1040
+ // family drops below. If a drop fails past this point the table stays
1041
+ // invisible, and the tombstone guarantees the drop completes on the
1042
+ // next startup (or on a same-name create).
1020
1043
  delete databases[databaseName][tableName];
1021
1044
  for (const entry of primaryStore.getRange({ versions: true, snapshot: false, lazy: true })) {
1022
1045
  if (entry.metadataFlags & HAS_BLOBS && entry.value) {
@@ -1024,14 +1047,28 @@ export function makeTable(options) {
1024
1047
  }
1025
1048
  }
1026
1049
  if (databaseName === databasePath) {
1027
- // part of a database
1050
+ // part of a database.
1051
+ // Drop the column families and AWAIT them. Previously the catalog
1052
+ // metadata was removed (synchronous removeSync) before primaryStore.drop(),
1053
+ // and the drops were fire-and-forget (their rejections swallowed). If a
1054
+ // column family was corrupt/half-initialized the drop would fail, yet the
1055
+ // catalog entry was already gone - an orphaned "ghost" column family that
1056
+ // poisons same-name recreates. By awaiting the drops before touching the
1057
+ // catalog, a failed drop surfaces as the operation's error and the
1058
+ // tombstoned catalog rows survive for the startup reconcile.
1059
+ const drops = [];
1028
1060
  for (const attribute of attributes) {
1029
- dbisDb.remove(TableResource.tableName + '/' + attribute.name);
1030
1061
  const index = indices[attribute.name];
1031
- index?.drop();
1062
+ if (index) drops.push(index.drop());
1063
+ }
1064
+ drops.push(primaryStore.drop());
1065
+ await Promise.all(drops);
1066
+ // Column families are gone; remove the catalog metadata (including
1067
+ // the tombstoned primary entry).
1068
+ for (const attribute of attributes) {
1069
+ dbisDb.remove(TableResource.tableName + '/' + attribute.name);
1032
1070
  }
1033
1071
  dbisDb.remove(TableResource.tableName + '/');
1034
- primaryStore.drop();
1035
1072
  await dbisDb.committed;
1036
1073
  } else {
1037
1074
  // legacy table per database
@@ -521,6 +521,27 @@ function initStores(
521
521
  Object.defineProperty(value, 'key', { value: key, configurable: true });
522
522
  }
523
523
 
524
+ // Complete any drops that were interrupted mid-flight. dropTable persists a
525
+ // `dropping` tombstone on the table's primary catalog entry before removing
526
+ // column families; if the process died or a column family drop failed
527
+ // partway, the tombstone survives alongside the catalog rows. Without this
528
+ // reconcile, those rows would silently resurrect the table below
529
+ // (recreating any missing column families as empty stores).
530
+ for (const [tableName, tableDef] of tablesToLoad) {
531
+ if (!tableDef.primary?.dropping) continue;
532
+ try {
533
+ completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName);
534
+ definedTables?.delete(tableName);
535
+ } catch (error) {
536
+ logger.error(
537
+ `Failed to complete interrupted drop of table ${databaseName}.${tableName}; will retry on next start`,
538
+ error
539
+ );
540
+ }
541
+ // whether or not cleanup succeeded, never load a table that was being dropped
542
+ tablesToLoad.delete(tableName);
543
+ }
544
+
524
545
  for (const [tableName, tableDef] of tablesToLoad) {
525
546
  let { attributes, primary: primaryAttribute } = tableDef;
526
547
  if (!primaryAttribute) {
@@ -1067,7 +1088,8 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1067
1088
  }
1068
1089
 
1069
1090
  exclusiveLock(); // get an exclusive lock on the database so we can verify that we are the only thread creating the table (and assigning the table id)
1070
- if ((attributesDbi as any).getSync(dbiName)) {
1091
+ const existingTableMeta = (attributesDbi as any).getSync(dbiName);
1092
+ if (existingTableMeta && !existingTableMeta.dropping) {
1071
1093
  // table was created while we were setting up
1072
1094
  if (releaseExclusiveLock) releaseExclusiveLock();
1073
1095
  resetDatabases();
@@ -1075,50 +1097,69 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1075
1097
  }
1076
1098
 
1077
1099
  let primaryStore;
1078
- if (rootStore instanceof RocksDatabase) {
1079
- primaryStore = openRocksDatabase(rootStore.path, { ...dbiInit, name: dbiName } as any);
1080
- } else {
1081
- primaryStore = (rootStore as any).openDB(dbiName, dbiInit as any);
1082
- }
1083
- primaryStore = handleLocalTimeForGets(primaryStore, rootStore);
1084
- rootStore.databaseName = databaseName;
1085
- primaryStore.tableId = attributesDbi.getSync(NEXT_TABLE_ID);
1086
- logger.trace(`Assigning new table id ${primaryStore.tableId} for ${tableName}`);
1087
- if (!primaryStore.tableId) primaryStore.tableId = 1;
1088
- attributesDbi.put(NEXT_TABLE_ID, primaryStore.tableId + 1);
1100
+ try {
1101
+ if (existingTableMeta?.dropping) {
1102
+ // A previous drop of this table was interrupted after its tombstone
1103
+ // was written. Complete it now (under the exclusive lock) so the
1104
+ // create below starts from a clean slate; treating the tombstoned
1105
+ // entry as an existing table would recurse forever on the stale
1106
+ // catalog row.
1107
+ completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName);
1108
+ }
1109
+ if (rootStore instanceof RocksDatabase) {
1110
+ primaryStore = openRocksDatabase(rootStore.path, { ...dbiInit, name: dbiName } as any);
1111
+ } else {
1112
+ primaryStore = (rootStore as any).openDB(dbiName, dbiInit as any);
1113
+ }
1114
+ primaryStore = handleLocalTimeForGets(primaryStore, rootStore);
1115
+ rootStore.databaseName = databaseName;
1116
+ primaryStore.tableId = attributesDbi.getSync(NEXT_TABLE_ID);
1117
+ logger.trace(`Assigning new table id ${primaryStore.tableId} for ${tableName}`);
1118
+ if (!primaryStore.tableId) primaryStore.tableId = 1;
1119
+ attributesDbi.put(NEXT_TABLE_ID, primaryStore.tableId + 1);
1089
1120
 
1090
- primaryKeyAttribute.tableId = primaryStore.tableId;
1091
- Table = setTable(
1092
- tables,
1093
- tableName,
1094
- makeTable({
1095
- primaryStore,
1096
- auditStore,
1097
- audit,
1098
- sealed,
1099
- splitSegments,
1100
- replicate,
1101
- trackDeletes,
1102
- expirationMS: expiration && expiration * 1000,
1103
- evictionMS: eviction && eviction * 1000,
1104
- primaryKey,
1121
+ primaryKeyAttribute.tableId = primaryStore.tableId;
1122
+ Table = setTable(
1123
+ tables,
1105
1124
  tableName,
1106
- tableId: primaryStore.tableId,
1107
- databasePath: databaseName,
1108
- databaseName,
1109
- indices: {},
1110
- attributes,
1111
- schemaDefined,
1112
- dbisDB: attributesDbi,
1113
- description,
1114
- properties,
1115
- hidden,
1116
- })
1117
- );
1118
- Table.schemaVersion = 1;
1119
- hasChanges = true;
1125
+ makeTable({
1126
+ primaryStore,
1127
+ auditStore,
1128
+ audit,
1129
+ sealed,
1130
+ splitSegments,
1131
+ replicate,
1132
+ trackDeletes,
1133
+ expirationMS: expiration && expiration * 1000,
1134
+ evictionMS: eviction && eviction * 1000,
1135
+ primaryKey,
1136
+ tableName,
1137
+ tableId: primaryStore.tableId,
1138
+ databasePath: databaseName,
1139
+ databaseName,
1140
+ indices: {},
1141
+ attributes,
1142
+ schemaDefined,
1143
+ dbisDB: attributesDbi,
1144
+ description,
1145
+ properties,
1146
+ hidden,
1147
+ })
1148
+ );
1149
+ Table.schemaVersion = 1;
1150
+ hasChanges = true;
1120
1151
 
1121
- attributesDbi.put(dbiName, primaryKeyAttribute);
1152
+ attributesDbi.put(dbiName, primaryKeyAttribute);
1153
+ } catch (error) {
1154
+ // A failure while opening/creating the column family or writing the
1155
+ // table id / catalog entry (e.g. into an env poisoned by a prior
1156
+ // dangling column family) must NOT leak the exclusive
1157
+ // 'update-attributes' spin lock. If it leaks, every subsequent
1158
+ // create_table / attribute update on this database spins forever
1159
+ // (a hard wedge that pins a worker at 100% CPU). Release before rethrow.
1160
+ if (releaseExclusiveLock) releaseExclusiveLock();
1161
+ throw error;
1162
+ }
1122
1163
  }
1123
1164
  const indices = Table.indices;
1124
1165
  if (!attributesDbi) {
@@ -1171,8 +1212,14 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1171
1212
  let attributeDescriptor = attributesDbi.getSync(dbiKey);
1172
1213
  if (attribute.isPrimaryKey) {
1173
1214
  attributeDescriptor = attributeDescriptor || attributesDbi.getSync((dbiKey = tableName + '/')) || {};
1215
+ // Persist schemaDefined when the explicit live value disagrees with disk. Without this,
1216
+ // a stale `false` (from a v4-era write or replicated event) survives every reload: the
1217
+ // in-memory re-assert in the existing-Table branch only fixes the worker that ran @table,
1218
+ // but other workers' next disk-load re-reads the stale value.
1219
+ const schemaDefinedMismatch = schemaDefinedExplicit && attributeDescriptor.schemaDefined !== schemaDefined;
1174
1220
  // primary key can't change indexing, but settings can change
1175
1221
  if (
1222
+ schemaDefinedMismatch ||
1176
1223
  (audit !== undefined && audit !== Table.audit) ||
1177
1224
  (sealed !== undefined && sealed !== Table.sealed) ||
1178
1225
  (replicate !== undefined && replicate !== Table.replicate) ||
@@ -1190,6 +1237,7 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1190
1237
  if (sealed !== undefined) updatedPrimaryAttribute.sealed = sealed;
1191
1238
  if (replicate !== undefined) updatedPrimaryAttribute.replicate = replicate;
1192
1239
  if (attribute.type) updatedPrimaryAttribute.type = attribute.type;
1240
+ if (schemaDefinedMismatch) updatedPrimaryAttribute.schemaDefined = schemaDefined;
1193
1241
  hasChanges = true; // send out notification of the change
1194
1242
  exclusiveLock();
1195
1243
  attributesDbi.put(dbiKey, updatedPrimaryAttribute);
@@ -1538,6 +1586,50 @@ async function runIndexing(Table, attributes, indicesToRemove) {
1538
1586
  }
1539
1587
  }
1540
1588
 
1589
+ /**
1590
+ * Completes a table drop that was interrupted after its `dropping` tombstone
1591
+ * was written: drops any surviving table stores and removes the table's
1592
+ * catalog rows. Called from the boot-time schema load and from the create
1593
+ * path when a same-named table is created over a tombstoned entry. Callers
1594
+ * are expected to hold the database's exclusive lock or be in single-threaded
1595
+ * startup; the per-store drops tolerate races (another worker may be
1596
+ * completing the same drop concurrently).
1597
+ */
1598
+ function completeInterruptedDrop(rootStore, attributesDbi, databaseName: string, tableName: string) {
1599
+ logger.warn(`Completing interrupted drop of table ${databaseName}.${tableName}`);
1600
+ if (rootStore instanceof RocksDatabase) {
1601
+ for (const columnName of (rootStore as any).columns) {
1602
+ if (columnName.startsWith(tableName + '/')) {
1603
+ try {
1604
+ const columnStore = openRocksDatabase(rootStore.path, { name: columnName } as any);
1605
+ columnStore.dropSync();
1606
+ columnStore.close();
1607
+ } catch (error) {
1608
+ logger.warn(`Failed dropping column family ${columnName} of ${databaseName}.${tableName}`, error);
1609
+ }
1610
+ }
1611
+ }
1612
+ } else {
1613
+ // LMDB reuses an existing named sub-database on open, so the stores must
1614
+ // be dropped too; removing only the catalog rows would let a same-name
1615
+ // recreate silently inherit the previous table's records.
1616
+ for (const { key, value } of attributesDbi.getRange({ start: tableName + '/', end: tableName + '0' })) {
1617
+ try {
1618
+ const objectStorage =
1619
+ value?.isPrimaryKey || (value?.indexed?.type && CUSTOM_INDEXES[value.indexed.type]?.useObjectStore);
1620
+ const store = (rootStore as any).openDB(key, createOpenDBIObject(!objectStorage, objectStorage) as any);
1621
+ // lmdb drop commits with the env's next transaction
1622
+ store.drop?.();
1623
+ } catch (error) {
1624
+ logger.warn(`Failed dropping store ${key} of ${databaseName}.${tableName}`, error);
1625
+ }
1626
+ }
1627
+ }
1628
+ for (const key of attributesDbi.getKeys({ start: tableName + '/', end: tableName + '0' })) {
1629
+ attributesDbi.remove(key);
1630
+ }
1631
+ }
1632
+
1541
1633
  export function dropTableMeta({ table: tableName, database: databaseName }) {
1542
1634
  const rootStore = database({ database: databaseName, table: tableName });
1543
1635
  const removals = [];
@@ -109,6 +109,9 @@ async function processGraphQLSchema(gqlContent, urlPath, filePath, resources) {
109
109
  for (const arg of directive.arguments) {
110
110
  typeDef[arg.name.value] = coerceDirectiveValue(arg.value);
111
111
  }
112
+ // @table is the canonical schema-defined declaration; pass the flag explicitly so
113
+ // the existing-Table re-assert in databases.ts::table() fires on every reload.
114
+ typeDef.schemaDefined = true;
112
115
  if (typeDef.schema) typeDef.database = typeDef.schema;
113
116
  if (!typeDef.table) typeDef.table = typeName;
114
117
  if (typeDef.audit) typeDef.audit = typeDef.audit !== 'false';