@harperfast/harper 5.2.0-alpha.6 → 5.2.0-beta.2
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/bin/cliOperations.ts +76 -12
- package/bin/run.ts +10 -0
- package/bin/status.ts +1 -1
- package/components/Application.ts +146 -83
- package/components/Scope.ts +4 -0
- package/components/componentLoader.ts +7 -0
- package/components/operations.js +21 -1
- package/config/configUtils.ts +139 -5
- package/config-app.schema.json +70 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +7 -0
- package/dist/bin/cliOperations.js +76 -12
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/run.js +9 -0
- package/dist/bin/run.js.map +1 -1
- package/dist/bin/status.js +1 -1
- package/dist/bin/status.js.map +1 -1
- package/dist/components/Application.d.ts +12 -5
- package/dist/components/Application.js +121 -60
- package/dist/components/Application.js.map +1 -1
- package/dist/components/Scope.d.ts +1 -0
- package/dist/components/Scope.js +4 -0
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +7 -0
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/operations.js +20 -1
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +31 -0
- package/dist/config/configUtils.js +127 -5
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +8 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +12 -0
- package/dist/resources/DatabaseTransaction.js +97 -0
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/RequestTarget.js +13 -3
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.js +21 -2
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/Table.d.ts +3 -1
- package/dist/resources/Table.js +48 -5
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/metadata.d.ts +3 -0
- package/dist/resources/analytics/metadata.js +3 -0
- package/dist/resources/analytics/metadata.js.map +1 -1
- package/dist/resources/analytics/write.js +22 -0
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/databases.js +21 -0
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/defineResource.js +20 -7
- package/dist/resources/defineResource.js.map +1 -1
- package/dist/resources/jsResource.d.ts +24 -0
- package/dist/resources/jsResource.js +58 -2
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/openApi.js +45 -20
- package/dist/resources/openApi.js.map +1 -1
- package/dist/resources/scheduler/CronExpression.d.ts +71 -0
- package/dist/resources/scheduler/CronExpression.js +367 -0
- package/dist/resources/scheduler/CronExpression.js.map +1 -0
- package/dist/resources/scheduler/engine.d.ts +91 -0
- package/dist/resources/scheduler/engine.js +767 -0
- package/dist/resources/scheduler/engine.js.map +1 -0
- package/dist/resources/scheduler/scheduler.d.ts +33 -0
- package/dist/resources/scheduler/scheduler.js +200 -0
- package/dist/resources/scheduler/scheduler.js.map +1 -0
- package/dist/security/auth.js +1 -0
- package/dist/security/auth.js.map +1 -1
- package/dist/security/jsLoader.js +8 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/keys.d.ts +32 -0
- package/dist/security/keys.js +147 -0
- package/dist/security/keys.js.map +1 -1
- package/dist/server/REST.js +67 -1
- package/dist/server/REST.js.map +1 -1
- package/dist/server/Server.d.ts +6 -0
- package/dist/server/Server.js.map +1 -1
- package/dist/server/http.d.ts +2 -0
- package/dist/server/http.js +139 -14
- package/dist/server/http.js.map +1 -1
- package/dist/server/operationsServer.js +3 -3
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/progressEmitter.js +5 -1
- package/dist/server/serverHelpers/progressEmitter.js.map +1 -1
- package/dist/server/threads/threadServer.js +9 -5
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/binder/bind.d.ts +72 -0
- package/dist/sqlEngine/binder/bind.js +289 -0
- package/dist/sqlEngine/binder/bind.js.map +1 -0
- package/dist/sqlEngine/config.d.ts +35 -0
- package/dist/sqlEngine/config.js +62 -0
- package/dist/sqlEngine/config.js.map +1 -0
- package/dist/sqlEngine/diff/differential.d.ts +23 -0
- package/dist/sqlEngine/diff/differential.js +90 -0
- package/dist/sqlEngine/diff/differential.js.map +1 -0
- package/dist/sqlEngine/errors.d.ts +22 -0
- package/dist/sqlEngine/errors.js +36 -0
- package/dist/sqlEngine/errors.js.map +1 -0
- package/dist/sqlEngine/executor/runMutation.d.ts +35 -0
- package/dist/sqlEngine/executor/runMutation.js +352 -0
- package/dist/sqlEngine/executor/runMutation.js.map +1 -0
- package/dist/sqlEngine/executor/runSelect.d.ts +7 -0
- package/dist/sqlEngine/executor/runSelect.js +14 -0
- package/dist/sqlEngine/executor/runSelect.js.map +1 -0
- package/dist/sqlEngine/expressions/compile.d.ts +24 -0
- package/dist/sqlEngine/expressions/compile.js +326 -0
- package/dist/sqlEngine/expressions/compile.js.map +1 -0
- package/dist/sqlEngine/functions/aggregates.d.ts +12 -0
- package/dist/sqlEngine/functions/aggregates.js +299 -0
- package/dist/sqlEngine/functions/aggregates.js.map +1 -0
- package/dist/sqlEngine/functions/registry.d.ts +36 -0
- package/dist/sqlEngine/functions/registry.js +33 -0
- package/dist/sqlEngine/functions/registry.js.map +1 -0
- package/dist/sqlEngine/functions/standard.d.ts +9 -0
- package/dist/sqlEngine/functions/standard.js +86 -0
- package/dist/sqlEngine/functions/standard.js.map +1 -0
- package/dist/sqlEngine/index.d.ts +29 -0
- package/dist/sqlEngine/index.js +62 -0
- package/dist/sqlEngine/index.js.map +1 -0
- package/dist/sqlEngine/logical/build.d.ts +20 -0
- package/dist/sqlEngine/logical/build.js +326 -0
- package/dist/sqlEngine/logical/build.js.map +1 -0
- package/dist/sqlEngine/logical/op.d.ts +105 -0
- package/dist/sqlEngine/logical/op.js +10 -0
- package/dist/sqlEngine/logical/op.js.map +1 -0
- package/dist/sqlEngine/optimizer/joinAnalysis.d.ts +37 -0
- package/dist/sqlEngine/optimizer/joinAnalysis.js +144 -0
- package/dist/sqlEngine/optimizer/joinAnalysis.js.map +1 -0
- package/dist/sqlEngine/optimizer/optimize.d.ts +13 -0
- package/dist/sqlEngine/optimizer/optimize.js +53 -0
- package/dist/sqlEngine/optimizer/optimize.js.map +1 -0
- package/dist/sqlEngine/optimizer/ruleEngine.d.ts +7 -0
- package/dist/sqlEngine/optimizer/ruleEngine.js +26 -0
- package/dist/sqlEngine/optimizer/ruleEngine.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/limitPushdown.d.ts +19 -0
- package/dist/sqlEngine/optimizer/rules/limitPushdown.js +55 -0
- package/dist/sqlEngine/optimizer/rules/limitPushdown.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/planJoins.d.ts +19 -0
- package/dist/sqlEngine/optimizer/rules/planJoins.js +62 -0
- package/dist/sqlEngine/optimizer/rules/planJoins.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/predicateNormalize.d.ts +18 -0
- package/dist/sqlEngine/optimizer/rules/predicateNormalize.js +80 -0
- package/dist/sqlEngine/optimizer/rules/predicateNormalize.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/predicatePushdown.d.ts +11 -0
- package/dist/sqlEngine/optimizer/rules/predicatePushdown.js +48 -0
- package/dist/sqlEngine/optimizer/rules/predicatePushdown.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/projectionPushdown.d.ts +23 -0
- package/dist/sqlEngine/optimizer/rules/projectionPushdown.js +160 -0
- package/dist/sqlEngine/optimizer/rules/projectionPushdown.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/sortFromIndex.d.ts +14 -0
- package/dist/sqlEngine/optimizer/rules/sortFromIndex.js +49 -0
- package/dist/sqlEngine/optimizer/rules/sortFromIndex.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/validateScannable.d.ts +15 -0
- package/dist/sqlEngine/optimizer/rules/validateScannable.js +60 -0
- package/dist/sqlEngine/optimizer/rules/validateScannable.js.map +1 -0
- package/dist/sqlEngine/optimizer/whereToConditions.d.ts +85 -0
- package/dist/sqlEngine/optimizer/whereToConditions.js +484 -0
- package/dist/sqlEngine/optimizer/whereToConditions.js.map +1 -0
- package/dist/sqlEngine/parser/ast.d.ts +139 -0
- package/dist/sqlEngine/parser/ast.js +10 -0
- package/dist/sqlEngine/parser/ast.js.map +1 -0
- package/dist/sqlEngine/parser/normalizer.d.ts +28 -0
- package/dist/sqlEngine/parser/normalizer.js +413 -0
- package/dist/sqlEngine/parser/normalizer.js.map +1 -0
- package/dist/sqlEngine/parser/parse.d.ts +14 -0
- package/dist/sqlEngine/parser/parse.js +20 -0
- package/dist/sqlEngine/parser/parse.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalDistinct.d.ts +9 -0
- package/dist/sqlEngine/physical/PhysicalDistinct.js +29 -0
- package/dist/sqlEngine/physical/PhysicalDistinct.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalFilter.d.ts +10 -0
- package/dist/sqlEngine/physical/PhysicalFilter.js +25 -0
- package/dist/sqlEngine/physical/PhysicalFilter.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalHashAggregate.d.ts +27 -0
- package/dist/sqlEngine/physical/PhysicalHashAggregate.js +100 -0
- package/dist/sqlEngine/physical/PhysicalHashAggregate.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalHashJoin.d.ts +29 -0
- package/dist/sqlEngine/physical/PhysicalHashJoin.js +93 -0
- package/dist/sqlEngine/physical/PhysicalHashJoin.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalIndexNestedLoopJoin.d.ts +36 -0
- package/dist/sqlEngine/physical/PhysicalIndexNestedLoopJoin.js +78 -0
- package/dist/sqlEngine/physical/PhysicalIndexNestedLoopJoin.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalIndexScan.d.ts +22 -0
- package/dist/sqlEngine/physical/PhysicalIndexScan.js +77 -0
- package/dist/sqlEngine/physical/PhysicalIndexScan.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalLimit.d.ts +5 -0
- package/dist/sqlEngine/physical/PhysicalLimit.js +28 -0
- package/dist/sqlEngine/physical/PhysicalLimit.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalNestedLoopJoin.d.ts +20 -0
- package/dist/sqlEngine/physical/PhysicalNestedLoopJoin.js +51 -0
- package/dist/sqlEngine/physical/PhysicalNestedLoopJoin.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalProject.d.ts +18 -0
- package/dist/sqlEngine/physical/PhysicalProject.js +117 -0
- package/dist/sqlEngine/physical/PhysicalProject.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalQualify.d.ts +13 -0
- package/dist/sqlEngine/physical/PhysicalQualify.js +29 -0
- package/dist/sqlEngine/physical/PhysicalQualify.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalSort.d.ts +12 -0
- package/dist/sqlEngine/physical/PhysicalSort.js +59 -0
- package/dist/sqlEngine/physical/PhysicalSort.js.map +1 -0
- package/dist/sqlEngine/physical/op.d.ts +11 -0
- package/dist/sqlEngine/physical/op.js +9 -0
- package/dist/sqlEngine/physical/op.js.map +1 -0
- package/dist/sqlEngine/physical/plan.d.ts +23 -0
- package/dist/sqlEngine/physical/plan.js +162 -0
- package/dist/sqlEngine/physical/plan.js.map +1 -0
- package/dist/sqlEngine/router.d.ts +26 -0
- package/dist/sqlEngine/router.js +48 -0
- package/dist/sqlEngine/router.js.map +1 -0
- package/dist/sqlEngine/types.d.ts +37 -0
- package/dist/sqlEngine/types.js +13 -0
- package/dist/sqlEngine/types.js.map +1 -0
- package/dist/sqlTranslator/index.js +7 -1
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/utility/common_utils.js +25 -0
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/install/installer.d.ts +9 -1
- package/dist/utility/install/installer.js +21 -0
- package/dist/utility/install/installer.js.map +1 -1
- package/dist/validation/configValidator.js +3 -0
- package/dist/validation/configValidator.js.map +1 -1
- package/dist/validation/deleteValidator.js +10 -2
- package/dist/validation/deleteValidator.js.map +1 -1
- package/npm-shrinkwrap.json +272 -230
- package/package.json +3 -3
- package/resources/DESIGN.md +17 -16
- package/resources/DatabaseTransaction.ts +95 -0
- package/resources/RequestTarget.ts +12 -3
- package/resources/Resource.ts +23 -2
- package/resources/Table.ts +57 -6
- package/resources/analytics/metadata.ts +3 -0
- package/resources/analytics/write.ts +23 -0
- package/resources/databases.ts +24 -0
- package/resources/defineResource.ts +17 -4
- package/resources/jsResource.ts +61 -2
- package/resources/openApi.ts +44 -19
- package/resources/scheduler/CronExpression.ts +394 -0
- package/resources/scheduler/engine.ts +812 -0
- package/resources/scheduler/scheduler.ts +236 -0
- package/security/auth.ts +1 -0
- package/security/jsLoader.ts +8 -0
- package/security/keys.ts +152 -0
- package/server/REST.ts +70 -1
- package/server/Server.ts +6 -0
- package/server/http.ts +122 -15
- package/server/operationsServer.ts +5 -3
- package/server/serverHelpers/progressEmitter.ts +5 -1
- package/server/threads/threadServer.js +9 -5
- package/sqlTranslator/index.ts +16 -6
- package/studio/web/assets/{Chat-CTjtL8Z4.js → Chat-DHP4XpID.js} +2 -2
- package/studio/web/assets/{Chat-CTjtL8Z4.js.map → Chat-DHP4XpID.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-CafHR4Ur.js → FloatingChat-CJ7PssCv.js} +4 -4
- package/studio/web/assets/{FloatingChat-CafHR4Ur.js.map → FloatingChat-CJ7PssCv.js.map} +1 -1
- package/studio/web/assets/{applications-Buh_q0Vj.js → applications-DxXiGpsR.js} +2 -2
- package/studio/web/assets/{applications-Buh_q0Vj.js.map → applications-DxXiGpsR.js.map} +1 -1
- package/studio/web/assets/{index-0hXeECkS.js → index-BdbBanDP.js} +6 -6
- package/studio/web/assets/{index-0hXeECkS.js.map → index-BdbBanDP.js.map} +1 -1
- package/studio/web/assets/{index.lazy-B00B7VBT.js → index.lazy-B2eH28zD.js} +4 -4
- package/studio/web/assets/{index.lazy-B00B7VBT.js.map → index.lazy-B2eH28zD.js.map} +1 -1
- package/studio/web/assets/{profile-Cg2wwYPn.js → profile-DK5hgucv.js} +2 -2
- package/studio/web/assets/{profile-Cg2wwYPn.js.map → profile-DK5hgucv.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-DCaDIvyB.js → setComponentFile-BVDWRYxx.js} +2 -2
- package/studio/web/assets/{setComponentFile-DCaDIvyB.js.map → setComponentFile-BVDWRYxx.js.map} +1 -1
- package/studio/web/assets/{setup-CAVcAQjK.js → setup-DJ9BInoK.js} +2 -2
- package/studio/web/assets/{setup-CAVcAQjK.js.map → setup-DJ9BInoK.js.map} +1 -1
- package/studio/web/assets/{status-BRXorNdD.js → status-B_qzmgfD.js} +2 -2
- package/studio/web/assets/{status-BRXorNdD.js.map → status-B_qzmgfD.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-Dy1D62vO.js → swagger-ui-react-DOL5jCqg.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-Dy1D62vO.js.map → swagger-ui-react-DOL5jCqg.js.map} +1 -1
- package/studio/web/assets/{tsMode-A8gbL74v.js → tsMode-DpxUxfTW.js} +2 -2
- package/studio/web/assets/{tsMode-A8gbL74v.js.map → tsMode-DpxUxfTW.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-cDodrVcQ.js → useEntityRestURL-CU_lY6XW.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-cDodrVcQ.js.map → useEntityRestURL-CU_lY6XW.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/common_utils.ts +26 -0
- package/utility/install/installer.ts +26 -1
- package/validation/configValidator.ts +3 -0
- package/validation/deleteValidator.ts +11 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harperfast/harper",
|
|
3
3
|
"description": "Harper is an open-source Node.js performance platform that unifies database, cache, application, and messaging layers into one in-memory process.",
|
|
4
|
-
"version": "5.2.0-
|
|
4
|
+
"version": "5.2.0-beta.2",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://harper.fast",
|
|
7
7
|
"bugs": {
|
|
@@ -167,14 +167,14 @@
|
|
|
167
167
|
},
|
|
168
168
|
"dependencies": {
|
|
169
169
|
"@aws-sdk/client-s3": "^3.1012.0",
|
|
170
|
-
"@aws-sdk/lib-storage": "3.
|
|
170
|
+
"@aws-sdk/lib-storage": "3.1086.0",
|
|
171
171
|
"@endo/static-module-record": "^1.1.2",
|
|
172
172
|
"@fastify/autoload": "^6.3.1",
|
|
173
173
|
"@fastify/compress": "^8.3.1",
|
|
174
174
|
"@fastify/cors": "^11.2.0",
|
|
175
175
|
"@fastify/static": "^9.1.3",
|
|
176
176
|
"@harperfast/extended-iterable": "^1.0.1",
|
|
177
|
-
"@harperfast/rocksdb-js": "^2.
|
|
177
|
+
"@harperfast/rocksdb-js": "^2.5.0",
|
|
178
178
|
"@harperfast/skills": "^1.10.8",
|
|
179
179
|
"@turf/area": "6.5.0",
|
|
180
180
|
"@turf/boolean-contains": "6.5.0",
|
package/resources/DESIGN.md
CHANGED
|
@@ -78,22 +78,23 @@ One giant `makeTable()` factory that returns a `TableResource extends Resource`
|
|
|
78
78
|
|
|
79
79
|
## "Where is X" cheat sheet
|
|
80
80
|
|
|
81
|
-
| Question
|
|
82
|
-
|
|
|
83
|
-
| How is a CRUD request authorized?
|
|
84
|
-
| Where does versioning / conflict resolution happen?
|
|
85
|
-
| How does `search()` choose an index?
|
|
86
|
-
| How are subscriptions replayed?
|
|
87
|
-
| How is the response body shaped (select clause)?
|
|
88
|
-
| Where is record-level TTL evaluated?
|
|
89
|
-
|
|
|
90
|
-
| How
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
| How does a
|
|
94
|
-
| How does
|
|
95
|
-
| How
|
|
96
|
-
| How is
|
|
81
|
+
| Question | Where |
|
|
82
|
+
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
83
|
+
| How is a CRUD request authorized? | `Table.ts → #section: authz-hooks`; defaults in `Resource.ts` (`allowRead` etc.) |
|
|
84
|
+
| Where does versioning / conflict resolution happen? | `Table.ts → _writeUpdate` (`#section: write-path-internals`) |
|
|
85
|
+
| How does `search()` choose an index? | `Table.ts → search` (`#section: search-query`) |
|
|
86
|
+
| How are subscriptions replayed? | `Table.ts → subscribe` (`#section: pub-sub`) |
|
|
87
|
+
| How is the response body shaped (select clause)? | `Table.ts → transformEntryForSelect` (`#section: search-query`) |
|
|
88
|
+
| Where is record-level TTL evaluated? | `Table.ts → setTTLExpiration` (`#section: lifecycle-admin`); `Updatable.getExpiresAt` (`#section: setup-and-factory`). Stored expiry metadata is resolved in the `_writeUpdate` commit closure: `options.expiresAt ?? context.expiresAt ?? (record @expiresAt field, if finite & ≥ 0) ?? table default`. This metadata drives read-hiding + the cleanup sweep. The `@expiresAt` attribute is authoritative for **direct** put/patch only; cache/source fills persist via `recordUpdater` and derive expiry from `sourceContext.expiresAt` (source freshness / table default), not the field. |
|
|
89
|
+
| Why does `search()` hide a row that's past its TTL but not yet swept? | `Table.ts → transformEntryForSelect` unconditionally treats `entry.expiresAt < Date.now()` as gone (lazy eviction on read) — correct for a SELECT, but a mutation locating rows to overwrite needs the opposite: pass `target.includeExpired = true` (read by the SQL engine's `runUpdate`/`runDelete` via `SqlEngineContext.includeExpiredRows`) to treat such a row as a live match, matching the leniency a direct by-id `put`/`patch` already has (they skip this check entirely, since `Resource.patch`'s static options don't request `ensureLoaded`). |
|
|
90
|
+
| How are residencies enforced (replication)? | `Table.ts → #section: lifecycle-admin` (residency block: `getResidencyRecord`, `setResidency`, `setResidencyById`, `getResidency`) |
|
|
91
|
+
| How is the RecordObject prototype applied? | `RecordEncoder.ts` (see `../DESIGN.md`) |
|
|
92
|
+
| Where is the per-request transaction stored? | `transaction.ts` + `contextStorage` (AsyncLocalStorage) |
|
|
93
|
+
| How does a query opt out of a read snapshot? | Pass `snapshot: false` on the search request (e.g. `get_analytics`). `Table.ts → search` calls `txn.useReadTxn(snapshot === false)`; on RocksDB `DatabaseTransaction.getReadTxn` then builds the read txn with `{ disableSnapshot: true }` so a long scan reads latest without pinning a snapshot. No-op on LMDB (`LMDBTransaction.useReadTxn`). |
|
|
94
|
+
| How does a URL path map to a Resource? | `Resources.ts → getMatch` (exact/prefix fast path) then `matchParamRoute` (parameterised routes); see "Path routing" below |
|
|
95
|
+
| How does HNSW keep the graph connected on delete? | `indexes/HierarchicalNavigableSmallWorld.ts → index()` delete path: zero-degree orphans reindexed via `needsReindexing`; severed multi-node islands detected and reconnected by `repairSeveredNeighbors` (#1712) |
|
|
96
|
+
| How is a filter applied _during_ a vector search? | Predicate-aware traversal (#1241): `search.ts → executeConditions` composes companion AND conditions + a request `vectorFilter` + a record-scoped `allowRead` override into one `(primaryKey) => boolean` (`composeRecordFilter`) and passes it to `HierarchicalNavigableSmallWorld.search(cond, ctx, filter)`. The filter gates result admission at layer 0 only (routing ignores it, ACORN-style); a visit budget (`filterExpansion`) bounds the under-filled/selective case. Very selective _condition_ filters are instead diverted to the exact brute-force path by the query planner's `estimateCountAsSort` ordering. |
|
|
97
|
+
| How is row-level read access control enforced? | Unified `allowRead` (#1422 gap 2 / #1241): an application-OVERRIDDEN `allowRead` (detected via the `isDefaultAllowRead` marker on the framework defaults) is record-scoped — evaluated once per record with `this` = the (frozen) record during query execution, fail-closed on throw, dispatched via the resolved method (never `record.allowRead` lookup — data shadowing). The authorize wrapper (`Resource.ts → authorizeActionOnResource`) defers collection reads on tables (`supportsRowLevelAllowRead`) to this per-record path; single-record `get` keeps the entry check (record loaded, proxied reads work). Records also expose a non-enumerable `allowRead` delegate on the per-table `structPrototype`. |
|
|
97
98
|
|
|
98
99
|
---
|
|
99
100
|
|
|
@@ -27,6 +27,80 @@ const MAX_RETRIES = 40;
|
|
|
27
27
|
// cap (see the commit rejection handler), don't grow the delay unbounded.
|
|
28
28
|
const MAX_RETRY_DELAY_MS = 1000;
|
|
29
29
|
let outstandingCommit, outstandingCommitStart;
|
|
30
|
+
|
|
31
|
+
// The analytics module registers a recorder here at load (dependency inversion, mirroring
|
|
32
|
+
// `replicationConfirmation` below) so the storage layer doesn't statically import the analytics/server
|
|
33
|
+
// modules. Unset until analytics loads, and when analytics is disabled the recorder call is cheap.
|
|
34
|
+
let recordCommitLatencyMs: ((durationMs: number) => void) | undefined;
|
|
35
|
+
export function setCommitLatencyRecorder(recorder: ((durationMs: number) => void) | undefined) {
|
|
36
|
+
recordCommitLatencyMs = recorder;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Emit the submit→settle duration of a write commit as the `transaction-commit-time` distribution
|
|
40
|
+
// metric. Recorded on both fulfilment and rejection since a slow-then-failed commit still consumed
|
|
41
|
+
// queue time. The recorder is wrapped so it can never throw — a metrics failure must neither break the
|
|
42
|
+
// commit nor surface as an unhandled rejection on this floating `.then`. The thenable guard protects
|
|
43
|
+
// against a future caller passing a non-Promise `commitResolution` (today it is always the rocksdb-js
|
|
44
|
+
// async `Transaction.commit()` result, which is guaranteed to be a Promise).
|
|
45
|
+
function recordCommitLatency(commitResolution: Promise<void>, submittedAt: number) {
|
|
46
|
+
if (!recordCommitLatencyMs) return;
|
|
47
|
+
const record = () => {
|
|
48
|
+
try {
|
|
49
|
+
recordCommitLatencyMs(performance.now() - submittedAt);
|
|
50
|
+
} catch {
|
|
51
|
+
// analytics recording is best-effort and must never disturb the commit path
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
if (commitResolution && typeof (commitResolution as any).then === 'function') {
|
|
55
|
+
commitResolution.then(record, record);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Queue-depth gauges surfaced through the analytics pipeline (write-transaction-queue-depth /
|
|
60
|
+
// read-transaction-queue-depth). Per-thread state; the analytics aggregator sums across threads.
|
|
61
|
+
// `writeTxnQueueDepth` counts write commits handed to the storage engine but not yet resolved —
|
|
62
|
+
// this is the backlog that, when it drains too slowly, produces the "Outstanding write transactions
|
|
63
|
+
// have too long of queue" overload error. Read depth is derived from the live `trackedTxns` set
|
|
64
|
+
// (every tracked transaction holds an open read snapshot). We also retain a high-water mark per
|
|
65
|
+
// sampling window because the queue can fill and drain within a single (~1s) analytics period, so an
|
|
66
|
+
// instantaneous sample taken at emit time would routinely miss the spike operators need to see.
|
|
67
|
+
// RocksDB-write-path only: LMDB routes through the separate LMDBTransaction.commit()/getReadTxn()
|
|
68
|
+
// overrides (resources/LMDBTransaction.ts), which maintain their own unrelated `trackedTxns` set and
|
|
69
|
+
// do not call into this accounting.
|
|
70
|
+
let writeTxnQueueDepth = 0;
|
|
71
|
+
let writeTxnQueueDepthHighWater = 0;
|
|
72
|
+
let readTxnQueueDepthHighWater = 0;
|
|
73
|
+
|
|
74
|
+
function enterWriteQueue() {
|
|
75
|
+
if (++writeTxnQueueDepth > writeTxnQueueDepthHighWater) writeTxnQueueDepthHighWater = writeTxnQueueDepth;
|
|
76
|
+
}
|
|
77
|
+
function leaveWriteQueue() {
|
|
78
|
+
// Floor at zero: accounting is balanced by construction (every enterWriteQueue has exactly one
|
|
79
|
+
// matching settlement), but the guard is cheap insurance against a future call-site imbalance
|
|
80
|
+
// producing a negative depth that would corrupt every subsequent sample.
|
|
81
|
+
if (writeTxnQueueDepth > 0) writeTxnQueueDepth--;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns the current write/read transaction queue depths for this thread along with the high-water
|
|
86
|
+
* mark observed since the previous call, then resets the high-water marks to the current depth so the
|
|
87
|
+
* next sampling window starts fresh. Consumed by the analytics writer (see analytics/write.ts).
|
|
88
|
+
*/
|
|
89
|
+
export function getTransactionQueueDepths() {
|
|
90
|
+
// `readTxnQueueDepthHighWater` is maintained at the single trackedTxns growth site, so it already
|
|
91
|
+
// dominates the current size here — no need to reconcile against `readDepth` before reporting.
|
|
92
|
+
const readDepth = trackedTxns.size;
|
|
93
|
+
const depths = {
|
|
94
|
+
writeDepth: writeTxnQueueDepth,
|
|
95
|
+
writeMaxDepth: writeTxnQueueDepthHighWater,
|
|
96
|
+
readDepth,
|
|
97
|
+
readMaxDepth: readTxnQueueDepthHighWater,
|
|
98
|
+
};
|
|
99
|
+
writeTxnQueueDepthHighWater = writeTxnQueueDepth;
|
|
100
|
+
readTxnQueueDepthHighWater = readDepth;
|
|
101
|
+
return depths;
|
|
102
|
+
}
|
|
103
|
+
|
|
30
104
|
let confirmReplication;
|
|
31
105
|
export function replicationConfirmation(callback) {
|
|
32
106
|
confirmReplication = callback;
|
|
@@ -161,6 +235,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
161
235
|
}
|
|
162
236
|
if ((this.transaction as any).openTimer) (this.transaction as any).openTimer = 0;
|
|
163
237
|
trackedTxns.add(this);
|
|
238
|
+
if (trackedTxns.size > readTxnQueueDepthHighWater) readTxnQueueDepthHighWater = trackedTxns.size;
|
|
164
239
|
return this.transaction;
|
|
165
240
|
}
|
|
166
241
|
|
|
@@ -329,6 +404,26 @@ export class DatabaseTransaction implements Transaction {
|
|
|
329
404
|
// cast to Promise<void> is safe — the sentinel never propagates
|
|
330
405
|
// past that branch.
|
|
331
406
|
commitResolution = transaction.commit() as Promise<void>;
|
|
407
|
+
// Record how long this commit stays outstanding (submit → settle) as a distribution
|
|
408
|
+
// metric. This is the same clock the overload check uses (outstandingCommitStart is
|
|
409
|
+
// stamped at submit), so a rising p99/p999 is the leading indicator for the
|
|
410
|
+
// "Outstanding write transactions have too long of queue" (503) rejection. A transient-
|
|
411
|
+
// conflict retry rejects this promise and issues a fresh commit(), and outstandingCommit
|
|
412
|
+
// re-arms per attempt, so recording per attempt matches the overload semantics.
|
|
413
|
+
recordCommitLatency(commitResolution, performance.now());
|
|
414
|
+
// Count this commit against the write queue depth until the storage engine
|
|
415
|
+
// resolves it. A transient-conflict retry rejects this promise and issues a
|
|
416
|
+
// fresh commit() (re-entering here), so the enter/leave stays balanced. leaveWriteQueue
|
|
417
|
+
// never throws, so the settled promise resolves and needs no rejection handling of its own.
|
|
418
|
+
// The thenable guard protects against a future caller passing a non-Promise
|
|
419
|
+
// `commitResolution` (today it is always rocksdb-js's async Transaction.commit()
|
|
420
|
+
// result, guaranteed to be a Promise).
|
|
421
|
+
enterWriteQueue();
|
|
422
|
+
if (commitResolution && typeof (commitResolution as any).then === 'function') {
|
|
423
|
+
commitResolution.then(leaveWriteQueue, leaveWriteQueue);
|
|
424
|
+
} else {
|
|
425
|
+
leaveWriteQueue();
|
|
426
|
+
}
|
|
332
427
|
} else {
|
|
333
428
|
try {
|
|
334
429
|
commitResolution = transaction.abort();
|
|
@@ -111,17 +111,26 @@ export class RequestTarget extends URLSearchParams {
|
|
|
111
111
|
if (path) {
|
|
112
112
|
// parse for properties and set the id
|
|
113
113
|
if (path.startsWith('/')) path = path.substring(1);
|
|
114
|
-
} else {
|
|
115
|
-
return; // leave
|
|
114
|
+
} else if (target === undefined) {
|
|
115
|
+
return; // constructed with no target at all (internal use) — leave id/isCollection unset
|
|
116
116
|
}
|
|
117
117
|
if (path) {
|
|
118
118
|
if (path.endsWith('/')) {
|
|
119
119
|
this.isCollection = true;
|
|
120
120
|
}
|
|
121
121
|
this.id = decodeURIComponent(path);
|
|
122
|
-
} else {
|
|
122
|
+
} else if (this.pathname === '/') {
|
|
123
|
+
// a bare trailing slash is the documented way to address a resource's collection
|
|
123
124
|
this.isCollection = true;
|
|
124
125
|
this.id = null;
|
|
126
|
+
} else {
|
|
127
|
+
// an exact resource-path match with nothing left to parse and no trailing slash
|
|
128
|
+
// (e.g. `/redirects` instead of the required `/redirects/`, harper#678): this is
|
|
129
|
+
// neither a valid collection request nor a specific record, so id/isCollection must
|
|
130
|
+
// still be well-defined (never left `undefined`) for dispatch to reject it cleanly
|
|
131
|
+
// instead of letting downstream code assume one of them is always set.
|
|
132
|
+
this.isCollection = false;
|
|
133
|
+
this.id = null;
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
toString() {
|
package/resources/Resource.ts
CHANGED
|
@@ -689,8 +689,11 @@ function transactional(
|
|
|
689
689
|
query = new RequestTarget();
|
|
690
690
|
query.id = id;
|
|
691
691
|
if (id == null) {
|
|
692
|
-
if (options.method === 'get') {
|
|
693
|
-
|
|
692
|
+
if (options.method === 'get' || options.method === 'delete') {
|
|
693
|
+
// For delete, a bare null/undefined id would be coerced into a whole-collection
|
|
694
|
+
// target and silently delete every record (studio#1199); a deliberate delete-all
|
|
695
|
+
// must use an explicit query object instead.
|
|
696
|
+
throw new ClientError(`Using an argument with a value of ${id} for ${options.method}, is not allowed`);
|
|
694
697
|
}
|
|
695
698
|
query.isCollection = true;
|
|
696
699
|
}
|
|
@@ -701,6 +704,24 @@ function transactional(
|
|
|
701
704
|
query.id = id;
|
|
702
705
|
}
|
|
703
706
|
isCollection = query.isCollection;
|
|
707
|
+
if (
|
|
708
|
+
options.method === 'post' &&
|
|
709
|
+
query.id === null &&
|
|
710
|
+
!isCollection &&
|
|
711
|
+
this.prototype.post === Resource.prototype.post
|
|
712
|
+
) {
|
|
713
|
+
// the matched path had nothing left to resolve into a collection or a specific record —
|
|
714
|
+
// i.e. it exactly matched a resource's base path without the required trailing slash
|
|
715
|
+
// (harper#678). This only matters for the base/default post() dispatch: it reads
|
|
716
|
+
// this.#isCollection (set from this same query) and falls back to missingMethod() for
|
|
717
|
+
// this state anyway, so rejecting early here just gives a clearer, purpose-built message.
|
|
718
|
+
// A resource with its own post() override (e.g. a component doing a bulk import via
|
|
719
|
+
// POST to its collection root, like the redirector template's Redirect.post()) is
|
|
720
|
+
// trusted to handle a null-id/non-collection target itself — it may not use id/isCollection
|
|
721
|
+
// at all, and forcing the trailing slash on it would break a currently-supported no-slash
|
|
722
|
+
// bulk-POST convention. See harper#678's regression on PR #1807.
|
|
723
|
+
throw new ClientError(`A trailing slash is required to POST to the ${this.name} collection`, 404);
|
|
724
|
+
}
|
|
704
725
|
let resourceOptions;
|
|
705
726
|
if (!context) {
|
|
706
727
|
// try to get the context from the async context if possible
|
package/resources/Table.ts
CHANGED
|
@@ -1244,7 +1244,7 @@ export function makeTable(options) {
|
|
|
1244
1244
|
// go back to the static search method so it gets a chance to override
|
|
1245
1245
|
return constructor.search(target, this.getContext());
|
|
1246
1246
|
}
|
|
1247
|
-
if (target && target.id
|
|
1247
|
+
if (target && target.id == null && !target.toString()) {
|
|
1248
1248
|
const description = {
|
|
1249
1249
|
// basically a describe call
|
|
1250
1250
|
records: './', // an href to the records themselves
|
|
@@ -2081,8 +2081,10 @@ export function makeTable(options) {
|
|
|
2081
2081
|
const type = fullUpdate ? 'put' : 'patch';
|
|
2082
2082
|
let residencyId: number | undefined;
|
|
2083
2083
|
if (options?.residencyId != undefined) residencyId = options.residencyId;
|
|
2084
|
-
|
|
2085
|
-
|
|
2084
|
+
// options/context expiresAt are the most specific overrides; a record @expiresAt field
|
|
2085
|
+
// (resolved below, once recordToStore is merged) overrides the table default in both
|
|
2086
|
+
// directions; the table default is the final fallback. -1 means no expiration.
|
|
2087
|
+
let expiresAt: number | undefined = options?.expiresAt ?? context?.expiresAt;
|
|
2086
2088
|
const additionalAuditRefs: Array<{ version: number; nodeId: number }> = []; // track additional audit refs to store
|
|
2087
2089
|
// Bulk base-copy snapshot apply: store current-state directly with no audit/transaction-log entry
|
|
2088
2090
|
// and no out-of-order resequencing/dedup (the source of the O(n) keyed-lookup spin in
|
|
@@ -2499,6 +2501,35 @@ export function makeTable(options) {
|
|
|
2499
2501
|
}
|
|
2500
2502
|
residencyId = getResidencyId(residency);
|
|
2501
2503
|
}
|
|
2504
|
+
if (expiresAt == undefined) {
|
|
2505
|
+
// A schema @expiresAt attribute makes the record field authoritative over the table
|
|
2506
|
+
// default, in both directions: stamp it into the stored expiry metadata that governs
|
|
2507
|
+
// read-hiding and the cleanup sweep, not just the separate index-pruning sweep (which
|
|
2508
|
+
// only removes already-past records and so can never extend past the table default).
|
|
2509
|
+
// Read from recordToStore so the metadata matches exactly what the pruning sweep later
|
|
2510
|
+
// reads back. Falls back to the table default when the field is unset or not a timestamp.
|
|
2511
|
+
const fieldExpiresAt = expiresAtProperty ? recordToStore?.[expiresAtProperty.name] : undefined;
|
|
2512
|
+
// Coerce only genuine timestamp shapes: a number/bigint epoch, a Date, or a numeric/ISO
|
|
2513
|
+
// string. Booleans, empty/whitespace strings, and null/undefined fall through to NaN so a
|
|
2514
|
+
// nonsensical field value uses the table default rather than expiring the record at epoch 0.
|
|
2515
|
+
let fieldExpiresAtMs = NaN;
|
|
2516
|
+
if (typeof fieldExpiresAt === 'number' || typeof fieldExpiresAt === 'bigint')
|
|
2517
|
+
fieldExpiresAtMs = Number(fieldExpiresAt);
|
|
2518
|
+
else if (fieldExpiresAt instanceof Date) fieldExpiresAtMs = fieldExpiresAt.getTime();
|
|
2519
|
+
else if (typeof fieldExpiresAt === 'string' && fieldExpiresAt.trim() !== '') {
|
|
2520
|
+
const numeric = Number(fieldExpiresAt);
|
|
2521
|
+
fieldExpiresAtMs = Number.isFinite(numeric) ? numeric : Date.parse(fieldExpiresAt);
|
|
2522
|
+
}
|
|
2523
|
+
// Only a finite, non-negative epoch counts: negatives collide with the -1 "no expiration"
|
|
2524
|
+
// sentinel (the encoder omits HAS_EXPIRATION for <0, but the field sweep would still evict a
|
|
2525
|
+
// negative field value), so treat a negative/NaN field as unset and use the table default.
|
|
2526
|
+
expiresAt =
|
|
2527
|
+
Number.isFinite(fieldExpiresAtMs) && fieldExpiresAtMs >= 0
|
|
2528
|
+
? fieldExpiresAtMs
|
|
2529
|
+
: expirationMs
|
|
2530
|
+
? expirationMs + Date.now()
|
|
2531
|
+
: -1;
|
|
2532
|
+
}
|
|
2502
2533
|
if (!fullUpdate) {
|
|
2503
2534
|
// we use our own data as the basis for the audit record, which will include information about the incremental updates, even if it was overwritten by CRDT resolution
|
|
2504
2535
|
auditRecordToStore = recordUpdate;
|
|
@@ -2989,6 +3020,14 @@ export function makeTable(options) {
|
|
|
2989
3020
|
// there, after materialization (the earlier evaluation stays as a prune that also bounds HNSW
|
|
2990
3021
|
// traversal). vectorFilter and condition filters intentionally keep the local-record
|
|
2991
3022
|
// semantics all query filters have on caching tables.
|
|
3023
|
+
//
|
|
3024
|
+
// A row that is past its TTL but not yet swept by the background eviction
|
|
3025
|
+
// scan is still physically present. A write that is about to overwrite it
|
|
3026
|
+
// anyway (e.g. the SQL engine locating UPDATE/DELETE targets) needs to see
|
|
3027
|
+
// it as a match — the same leniency a direct by-id put/patch already gets,
|
|
3028
|
+
// since those never run the ensureLoaded-gated freshness check this transform
|
|
3029
|
+
// otherwise applies unconditionally to every read.
|
|
3030
|
+
const includeExpired = (target as any).includeExpired === true;
|
|
2992
3031
|
const transformToRecord = TableResource.transformEntryForSelect(
|
|
2993
3032
|
select,
|
|
2994
3033
|
context,
|
|
@@ -2996,7 +3035,8 @@ export function makeTable(options) {
|
|
|
2996
3035
|
filtered,
|
|
2997
3036
|
ensure_loaded,
|
|
2998
3037
|
true,
|
|
2999
|
-
recordGuard
|
|
3038
|
+
recordGuard,
|
|
3039
|
+
includeExpired
|
|
3000
3040
|
);
|
|
3001
3041
|
let results = TableResource.transformToOrderedSelect(
|
|
3002
3042
|
entries,
|
|
@@ -3210,9 +3250,20 @@ export function makeTable(options) {
|
|
|
3210
3250
|
* @param recordGuard record-level read check (#1241) applied to the record actually being
|
|
3211
3251
|
* returned — i.e. AFTER any caching-source revalidation replaces a stale local copy — so an
|
|
3212
3252
|
* authorization verdict can't be made on bytes that differ from what the caller receives.
|
|
3253
|
+
* @param includeExpired when true, a row past its TTL but not yet swept is treated as a live
|
|
3254
|
+
* match rather than gone (used by the SQL engine's UPDATE/DELETE row-finder).
|
|
3213
3255
|
* @returns
|
|
3214
3256
|
*/
|
|
3215
|
-
static transformEntryForSelect(
|
|
3257
|
+
static transformEntryForSelect(
|
|
3258
|
+
select,
|
|
3259
|
+
context,
|
|
3260
|
+
readTxn,
|
|
3261
|
+
filtered,
|
|
3262
|
+
ensure_loaded?,
|
|
3263
|
+
canSkip?,
|
|
3264
|
+
recordGuard?,
|
|
3265
|
+
includeExpired?
|
|
3266
|
+
) {
|
|
3216
3267
|
let checkLoaded;
|
|
3217
3268
|
if (
|
|
3218
3269
|
ensure_loaded &&
|
|
@@ -3260,7 +3311,7 @@ export function makeTable(options) {
|
|
|
3260
3311
|
}
|
|
3261
3312
|
if (
|
|
3262
3313
|
(checkLoaded && entry?.metadataFlags & (INVALIDATED | EVICTED)) || // invalidated or evicted should go to load from source
|
|
3263
|
-
(entry?.expiresAt != undefined && entry?.expiresAt < Date.now())
|
|
3314
|
+
(!includeExpired && entry?.expiresAt != undefined && entry?.expiresAt < Date.now())
|
|
3264
3315
|
) {
|
|
3265
3316
|
// should expiration really apply?
|
|
3266
3317
|
if (context.onlyIfCached) {
|
|
@@ -8,6 +8,9 @@ export const METRIC = {
|
|
|
8
8
|
NODE_STORAGE: 'node-storage',
|
|
9
9
|
ROCKSDB_STATS: 'rocksdb-stats',
|
|
10
10
|
ROCKSDB_TXNLOG_STATS: 'rocksdb-txnlog-stats',
|
|
11
|
+
TRANSACTION_COMMIT_TIME: 'transaction-commit-time',
|
|
12
|
+
WRITE_TRANSACTION_QUEUE_DEPTH: 'write-transaction-queue-depth',
|
|
13
|
+
READ_TRANSACTION_QUEUE_DEPTH: 'read-transaction-queue-depth',
|
|
11
14
|
} as const;
|
|
12
15
|
|
|
13
16
|
export type BuiltInMetricName = (typeof METRIC)[keyof typeof METRIC];
|
|
@@ -14,6 +14,7 @@ import { server } from '../../server/Server.ts';
|
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
15
|
import { getAnalyticsHostnameTable, nodeIds, stableNodeId } from './hostnames.ts';
|
|
16
16
|
import { METRIC } from './metadata.ts';
|
|
17
|
+
import { getTransactionQueueDepths, setCommitLatencyRecorder } from '../DatabaseTransaction.ts';
|
|
17
18
|
import { RocksDatabase, type TransactionLogStats } from '@harperfast/rocksdb-js';
|
|
18
19
|
|
|
19
20
|
const log = forComponent('analytics').conditional;
|
|
@@ -130,6 +131,9 @@ export function recordAction(value: Value, metric: string, path?: string, method
|
|
|
130
131
|
|
|
131
132
|
server.recordAnalytics = recordAction;
|
|
132
133
|
|
|
134
|
+
// Let the storage layer emit write-commit latency without statically depending on this module.
|
|
135
|
+
setCommitLatencyRecorder((durationMs) => recordAction(durationMs, METRIC.TRANSACTION_COMMIT_TIME));
|
|
136
|
+
|
|
133
137
|
export function recordActionBinary(value, metric, path?, method?, type?) {
|
|
134
138
|
recordAction(Boolean(value), metric, path, method, type);
|
|
135
139
|
}
|
|
@@ -213,6 +217,25 @@ function sendAnalytics() {
|
|
|
213
217
|
byThread: true,
|
|
214
218
|
...memoryUsage,
|
|
215
219
|
});
|
|
220
|
+
// Transaction queue depth gauges. `depth` is the instantaneous depth at emit time; `maxDepth` is
|
|
221
|
+
// the high-water mark over this sampling window (the queue can fill and drain within a single
|
|
222
|
+
// period, so the instantaneous sample alone would miss short spikes). Reported per-thread and
|
|
223
|
+
// summed across threads by the aggregator, mirroring the `memory` gauge above.
|
|
224
|
+
const queueDepths = getTransactionQueueDepths();
|
|
225
|
+
metrics.push({
|
|
226
|
+
metric: METRIC.WRITE_TRANSACTION_QUEUE_DEPTH,
|
|
227
|
+
threadId,
|
|
228
|
+
byThread: true,
|
|
229
|
+
depth: queueDepths.writeDepth,
|
|
230
|
+
maxDepth: queueDepths.writeMaxDepth,
|
|
231
|
+
});
|
|
232
|
+
metrics.push({
|
|
233
|
+
metric: METRIC.READ_TRANSACTION_QUEUE_DEPTH,
|
|
234
|
+
threadId,
|
|
235
|
+
byThread: true,
|
|
236
|
+
depth: queueDepths.readDepth,
|
|
237
|
+
maxDepth: queueDepths.readMaxDepth,
|
|
238
|
+
});
|
|
216
239
|
for (const listener of analyticsListeners) {
|
|
217
240
|
listener(metrics);
|
|
218
241
|
}
|
package/resources/databases.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { makeTable } from './Table.ts';
|
|
|
13
13
|
import OpenEnvironmentObject from '../utility/lmdb/OpenEnvironmentObject.ts';
|
|
14
14
|
import { CONFIG_PARAMS, LEGACY_DATABASES_DIR_NAME, DATABASES_DIR_NAME } from '../utility/hdbTerms.ts';
|
|
15
15
|
import { getConfigPath } from '../config/configUtils.ts';
|
|
16
|
+
import { ClientError } from '../utility/errors/hdbError.ts';
|
|
16
17
|
import { _assignPackageExport } from '../globals.js';
|
|
17
18
|
import { getIndexedValues } from '../utility/lmdb/commonUtility.ts';
|
|
18
19
|
import * as signalling from '../utility/signalling.ts';
|
|
@@ -1120,6 +1121,29 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1120
1121
|
if (Table.primaryStore.rootStore.status === 'closed') {
|
|
1121
1122
|
throw new Error(`Can not use a closed data store from ${tableName} class`);
|
|
1122
1123
|
}
|
|
1124
|
+
// Reject moving the primary key to a different attribute on a table that already has records.
|
|
1125
|
+
// The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
|
|
1126
|
+
// leave describe reporting the new attribute while every record — old and newly inserted — stays
|
|
1127
|
+
// keyed by the original one; search_by_id/update/delete by the declared key then all miss. Only
|
|
1128
|
+
// schema-authored callers (@table / defineTable / create_table) reassert the declaration, so
|
|
1129
|
+
// gate on schemaDefinedExplicit to leave cluster schema-replication / data-loader callers alone.
|
|
1130
|
+
// See HarperFast/studio#1199.
|
|
1131
|
+
const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name;
|
|
1132
|
+
if (schemaDefinedExplicit && declaredPrimaryKey && declaredPrimaryKey !== Table.primaryKey) {
|
|
1133
|
+
let hasRecords = false;
|
|
1134
|
+
for (const _entry of Table.primaryStore.getRange({ start: true })) {
|
|
1135
|
+
hasRecords = true;
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
if (hasRecords) {
|
|
1139
|
+
throw new ClientError(
|
|
1140
|
+
`Cannot change the primary key of table '${databaseName}.${tableName}' from '${Table.primaryKey}' to ` +
|
|
1141
|
+
`'${declaredPrimaryKey}' because it already contains records. Recreate the table with the new primary ` +
|
|
1142
|
+
`key, or migrate the existing records.`,
|
|
1143
|
+
400
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1123
1147
|
// it table already exists, get the split segments setting
|
|
1124
1148
|
if (splitSegments == undefined) splitSegments = Table.splitSegments;
|
|
1125
1149
|
Table.attributes.splice(0, Table.attributes.length, ...attributes);
|
|
@@ -550,12 +550,25 @@ function applyContractMetadata(carrier: any, contract: Contract): void {
|
|
|
550
550
|
*/
|
|
551
551
|
function wrapStaticVerb(original: Function, compiled: CompiledVerb): Function {
|
|
552
552
|
const { queryFragment, bodyFragment, hasBody } = compiled;
|
|
553
|
+
function validateAndDispatch(this: any, target: any, rest: any[], body: any) {
|
|
554
|
+
const issues: ValidationIssue[] = [];
|
|
555
|
+
if (queryFragment) coerceAndValidateQuery(queryFragment, target, issues);
|
|
556
|
+
if (hasBody && bodyFragment) rest[0] = validateBody(bodyFragment, body, issues);
|
|
557
|
+
if (issues.length) throw new ValidationError(issues);
|
|
558
|
+
return original.call(this, target, ...rest);
|
|
559
|
+
}
|
|
553
560
|
return function (this: any, target: any, ...rest: any[]) {
|
|
554
561
|
if (target instanceof URLSearchParams) {
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
562
|
+
const body = rest[0];
|
|
563
|
+
// REST.ts's streaming deserializer hands the body over as a still-pending promise
|
|
564
|
+
// (`request.data`); await it before validating, or it reads as an empty object and every
|
|
565
|
+
// declared-required field is (incorrectly) reported missing. A plain value (as passed by a
|
|
566
|
+
// programmatic call or a unit test) validates synchronously, preserving the synchronous-throw
|
|
567
|
+
// contract those callers rely on.
|
|
568
|
+
if (hasBody && bodyFragment && body && typeof body.then === 'function') {
|
|
569
|
+
return body.then((resolvedBody: any) => validateAndDispatch.call(this, target, rest, resolvedBody));
|
|
570
|
+
}
|
|
571
|
+
return validateAndDispatch.call(this, target, rest, body);
|
|
559
572
|
}
|
|
560
573
|
return original.call(this, target, ...rest);
|
|
561
574
|
};
|
package/resources/jsResource.ts
CHANGED
|
@@ -71,9 +71,39 @@ export class ResourceLoadError extends Error {
|
|
|
71
71
|
*
|
|
72
72
|
* Thus, this plugin only handle files as they are added (`add` event). All other events result in a restart request.
|
|
73
73
|
*
|
|
74
|
+
* A redeploy tears down and reinstalls the component's files while this scope's watcher is paused
|
|
75
|
+
* (see `Scope`/`EntryHandler` deploy lifecycle); on resume the fresh chokidar scan re-emits every
|
|
76
|
+
* existing file as `'add'` — including ones whose contents just changed. Treating those as plain
|
|
77
|
+
* adds would silently re-run against the stale module cache and never flag a restart (harper#1817).
|
|
78
|
+
* So we track which files this scope has already loaded: a re-`add` of a known file is a redeploy of
|
|
79
|
+
* loaded code we cannot hot-swap, and is handled like a `change` — request a restart. A first-time
|
|
80
|
+
* `add` (initial load, or a genuinely new file added at runtime) still loads without a restart.
|
|
81
|
+
*
|
|
82
|
+
* A redeploy that *deletes* a loaded file is a different shape of the same problem: the fresh
|
|
83
|
+
* chokidar scan only reports what's currently on disk, so a file that's gone produces no event at
|
|
84
|
+
* all — no re-`add`, no `unlink` — and the modified-file handling above never sees it. Left
|
|
85
|
+
* unhandled, the deleted resource stays registered and active in memory (harper#1817 follow-up). So
|
|
86
|
+
* we also track which files the post-redeploy scan pass reports, and once that scan's `ready` fires,
|
|
87
|
+
* diff it against everything this scope has ever loaded: anything missing was deleted, and is
|
|
88
|
+
* handled the same way as a modified file — request a restart.
|
|
89
|
+
*
|
|
90
|
+
* That diff must only run for an actual redeploy rescan, not every time `EntryHandler` emits
|
|
91
|
+
* `ready` — it also refires after each ordinary runtime add/change once that file's read settles
|
|
92
|
+
* (its initial-scan-complete latch never resets outside a full rescan), and diffing against that
|
|
93
|
+
* would falsely treat every other already-loaded file as deleted. So the diff window is gated by
|
|
94
|
+
* the scope's own `deploy:start`/`deploy:end` bracket (see `Scope`): `deploy:start` pauses the
|
|
95
|
+
* watcher and opens the window (and is where we reset the scan-file tracking, since no file events
|
|
96
|
+
* can land while paused), and the first `ready` afterward — the resumed watcher's fresh scan
|
|
97
|
+
* completing — closes it and runs the diff.
|
|
74
98
|
*/
|
|
75
99
|
export async function handleApplication(scope: Scope) {
|
|
76
|
-
|
|
100
|
+
const loadedResourceFiles = new Set<string>();
|
|
101
|
+
// Files reported as `add` since the most recent `deploy:start`, populated only while
|
|
102
|
+
// `awaitingPostRedeployScan` is true — see the gating note above.
|
|
103
|
+
let currentScanFiles = new Set<string>();
|
|
104
|
+
let awaitingPostRedeployScan = false;
|
|
105
|
+
|
|
106
|
+
const entryHandler = scope.handleEntry(async function handleResourceEntry(entryEvent) {
|
|
77
107
|
if (entryEvent.entryType !== 'file') {
|
|
78
108
|
scope.logger.warn(
|
|
79
109
|
`jsResource plugin cannot handle entry type ${entryEvent.entryType}. Modify the 'files' option in ${scope.configFilePath} to only include files.`
|
|
@@ -81,7 +111,13 @@ export async function handleApplication(scope: Scope) {
|
|
|
81
111
|
return;
|
|
82
112
|
}
|
|
83
113
|
|
|
84
|
-
if (entryEvent.eventType
|
|
114
|
+
if (awaitingPostRedeployScan && entryEvent.eventType === 'add') {
|
|
115
|
+
// Recorded unconditionally — before the loaded/re-add branch below — so the post-scan
|
|
116
|
+
// deletion diff sees every file this scan reported, whether newly loaded or already known.
|
|
117
|
+
currentScanFiles.add(entryEvent.absolutePath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (entryEvent.eventType !== 'add' || loadedResourceFiles.has(entryEvent.absolutePath)) {
|
|
85
121
|
scope.requestRestart();
|
|
86
122
|
return;
|
|
87
123
|
}
|
|
@@ -97,6 +133,9 @@ export async function handleApplication(scope: Scope) {
|
|
|
97
133
|
scope.logger.debug?.(`Registered root resource: ${path}`);
|
|
98
134
|
}
|
|
99
135
|
recurseForResources(scope, resourceModule, root);
|
|
136
|
+
// Record the load so a later re-`add` of this same file (a redeploy re-scan) is treated
|
|
137
|
+
// as a change and requests a restart rather than silently re-serving stale cached code.
|
|
138
|
+
loadedResourceFiles.add(entryEvent.absolutePath);
|
|
100
139
|
// A JS resource that extends an exported @table is the one carrying author opt-ins
|
|
101
140
|
// (`static mcpTools`/`mcpPrompts`), and it registers here — after the schema-derived
|
|
102
141
|
// table class and after the MCP component's boot scan. Signal so listing surfaces
|
|
@@ -107,6 +146,26 @@ export async function handleApplication(scope: Scope) {
|
|
|
107
146
|
throw new ResourceLoadError(entryEvent.absolutePath, error);
|
|
108
147
|
}
|
|
109
148
|
});
|
|
149
|
+
|
|
150
|
+
// Optional chaining: a mock/test scope may not implement EventEmitter, and Scope#handleEntry
|
|
151
|
+
// itself can return undefined (e.g. MissingDefaultFilesOptionError). In real use `scope` is
|
|
152
|
+
// always an EventEmitter and `entryHandler` is always the EntryHandler backing this watcher.
|
|
153
|
+
scope.on?.('deploy:start', () => {
|
|
154
|
+
awaitingPostRedeployScan = true;
|
|
155
|
+
currentScanFiles = new Set();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
entryHandler?.on?.('ready', () => {
|
|
159
|
+
if (!awaitingPostRedeployScan) return;
|
|
160
|
+
awaitingPostRedeployScan = false;
|
|
161
|
+
for (const loadedFile of loadedResourceFiles) {
|
|
162
|
+
if (!currentScanFiles.has(loadedFile)) {
|
|
163
|
+
// Known file that the just-completed scan never reported — deleted during the redeploy.
|
|
164
|
+
loadedResourceFiles.delete(loadedFile);
|
|
165
|
+
scope.requestRestart();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
110
169
|
}
|
|
111
170
|
|
|
112
171
|
function recurseForResources(scope: Scope, resourceModule: any, prefix: string) {
|