@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4
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 +25 -3
- package/bin/run.ts +18 -0
- package/components/Application.ts +52 -78
- package/components/DESIGN.md +76 -3
- package/components/Scope.ts +10 -4
- package/components/anthropic/index.ts +10 -1
- package/components/bedrock/index.ts +10 -1
- package/components/componentLoader.ts +23 -14
- package/components/mcp/tools/operations.ts +3 -1
- package/components/ollama/index.ts +10 -1
- package/components/openai/index.ts +265 -18
- package/components/operations.js +40 -1
- package/config/configUtils.ts +3 -19
- package/dataLayer/DESIGN.md +29 -5
- package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
- package/dataLayer/restoreMarker.ts +250 -14
- package/dataLayer/schema.ts +2 -7
- package/dataLayer/schemaDescribe.ts +10 -0
- package/dist/bin/cliOperations.js +28 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/run.js +19 -0
- package/dist/bin/run.js.map +1 -1
- package/dist/components/Application.d.ts +8 -2
- package/dist/components/Application.js +43 -69
- package/dist/components/Application.js.map +1 -1
- package/dist/components/Scope.d.ts +1 -1
- package/dist/components/Scope.js +9 -4
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +0 -10
- package/dist/components/anthropic/index.js +9 -1
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +0 -12
- package/dist/components/bedrock/index.js +9 -1
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.js +23 -14
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +1 -1
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/ollama/index.d.ts +0 -13
- package/dist/components/ollama/index.js +9 -1
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +11 -15
- package/dist/components/openai/index.js +224 -12
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +9 -0
- package/dist/components/operations.js +32 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +1 -2
- package/dist/config/configUtils.js +7 -22
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +25 -3
- package/dist/dataLayer/restoreMarker.js +213 -13
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/schema.js +1 -4
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +10 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +28 -0
- package/dist/resources/DatabaseTransaction.d.ts +21 -1
- package/dist/resources/DatabaseTransaction.js +147 -8
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +1 -1
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/Table.d.ts +31 -3
- package/dist/resources/Table.js +397 -79
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +16 -10
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +1 -0
- package/dist/resources/blob.js +14 -5
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.js +3 -3
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databaseDropPreparation.d.ts +20 -0
- package/dist/resources/databaseDropPreparation.js +106 -0
- package/dist/resources/databaseDropPreparation.js.map +1 -0
- package/dist/resources/databases.d.ts +26 -21
- package/dist/resources/databases.js +1040 -204
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +23 -0
- package/dist/resources/derivedIndexRegistry.js +92 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -1
- package/dist/resources/derivedIndexRuntime.d.ts +10 -0
- package/dist/resources/derivedIndexRuntime.js +136 -25
- package/dist/resources/derivedIndexRuntime.js.map +1 -1
- package/dist/resources/derivedIndexes.d.ts +39 -0
- package/dist/resources/derivedIndexes.js +733 -0
- package/dist/resources/derivedIndexes.js.map +1 -0
- package/dist/resources/fullTextSchema.d.ts +8 -0
- package/dist/resources/fullTextSchema.js +42 -0
- package/dist/resources/fullTextSchema.js.map +1 -1
- package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
- package/dist/resources/fullTextSchemaLifecycle.js +0 -9
- package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
- package/dist/resources/graphql.js +101 -9
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
- package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
- package/dist/resources/models/Models.d.ts +26 -3
- package/dist/resources/models/Models.js +256 -13
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/TestBackend.d.ts +3 -1
- package/dist/resources/models/TestBackend.js +35 -1
- package/dist/resources/models/TestBackend.js.map +1 -1
- package/dist/resources/models/analyticsTable.d.ts +2 -2
- package/dist/resources/models/analyticsTable.js +4 -2
- package/dist/resources/models/analyticsTable.js.map +1 -1
- package/dist/resources/models/backendHelpers.d.ts +24 -0
- package/dist/resources/models/backendHelpers.js +18 -1
- package/dist/resources/models/backendHelpers.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +10 -21
- package/dist/resources/models/backendRegistry.js +74 -18
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +7 -0
- package/dist/resources/models/bootstrap.js +36 -4
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/decideHook.d.ts +33 -0
- package/dist/resources/models/decideHook.js +105 -0
- package/dist/resources/models/decideHook.js.map +1 -0
- package/dist/resources/models/decision.d.ts +64 -0
- package/dist/resources/models/decision.js +507 -0
- package/dist/resources/models/decision.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +134 -0
- package/dist/resources/models/decisionStore.js +350 -0
- package/dist/resources/models/decisionStore.js.map +1 -0
- package/dist/resources/models/embedHook.d.ts +66 -8
- package/dist/resources/models/embedHook.js +228 -61
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/generativeDecision.d.ts +58 -0
- package/dist/resources/models/generativeDecision.js +315 -0
- package/dist/resources/models/generativeDecision.js.map +1 -0
- package/dist/resources/models/types.d.ts +232 -4
- package/dist/security/authn/oidc/tokenExchange.js +3 -33
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
- package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
- package/dist/security/authn/oidc/tokenUseTable.js +28 -0
- package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
- package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
- package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
- package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
- package/dist/security/certificateVerification/crlVerification.js +77 -165
- package/dist/security/certificateVerification/crlVerification.js.map +1 -1
- package/dist/security/certificateVerification/types.d.ts +0 -3
- package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
- package/dist/security/certificateVerification/verificationTables.js +125 -0
- package/dist/security/certificateVerification/verificationTables.js.map +1 -0
- package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
- package/dist/security/certificateVerification/verificationUtils.js +6 -31
- package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
- package/dist/security/jsLoader.d.ts +5 -0
- package/dist/security/jsLoader.js +8 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/Server.d.ts +1 -0
- package/dist/server/Server.js.map +1 -1
- package/dist/server/http.js +2 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +21 -2
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobOwnership.d.ts +25 -0
- package/dist/server/jobs/jobOwnership.js +127 -0
- package/dist/server/jobs/jobOwnership.js.map +1 -0
- package/dist/server/jobs/jobProcess.js +9 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +18 -5
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/loadRootComponents.js +10 -0
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +11 -1
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +12 -2
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/threads/itc.d.ts +4 -1
- package/dist/server/threads/itc.js +29 -6
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +12 -4
- package/dist/server/threads/manageThreads.js +241 -119
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +1 -0
- package/dist/server/threads/threadServer.js +15 -1
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/server/threads/windowsProcessTree.d.ts +91 -0
- package/dist/server/threads/windowsProcessTree.js +361 -0
- package/dist/server/threads/windowsProcessTree.js.map +1 -0
- package/dist/server/throttle.d.ts +1 -1
- package/dist/server/throttle.js +8 -1
- package/dist/server/throttle.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +53 -46
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +10 -1
- package/dist/utility/errors/hdbError.js +55 -11
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/fsync.d.ts +2 -0
- package/dist/utility/fsync.js +20 -0
- package/dist/utility/fsync.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +3 -0
- package/dist/utility/hdbTerms.js +3 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/signalling.d.ts +7 -1
- package/dist/utility/signalling.js +27 -3
- package/dist/utility/signalling.js.map +1 -1
- package/dist/utility/when.d.ts +1 -0
- package/dist/utility/when.js +21 -0
- package/dist/utility/when.js.map +1 -1
- package/dist/validation/configValidator.js +30 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +13 -0
- package/json/systemSchema.json +28 -0
- package/npm-shrinkwrap.json +151 -55
- package/package.json +14 -13
- package/resources/DESIGN.md +80 -100
- package/resources/DatabaseTransaction.ts +157 -10
- package/resources/PrimaryRocksDatabase.ts +2 -2
- package/resources/Table.ts +494 -101
- package/resources/auditStore.ts +18 -10
- package/resources/blob.ts +13 -6
- package/resources/branchDatabase.ts +3 -3
- package/resources/databaseDropPreparation.ts +120 -0
- package/resources/databases.ts +1155 -205
- package/resources/derivedIndexRegistry.ts +122 -0
- package/resources/derivedIndexRuntime.ts +157 -30
- package/resources/derivedIndexes.ts +856 -0
- package/resources/fullTextSchema.ts +55 -0
- package/resources/fullTextSchemaLifecycle.ts +0 -15
- package/resources/graphql.ts +107 -12
- package/resources/indexes/DESIGN.md +7 -0
- package/resources/indexes/hnswDerivedIndex.ts +0 -260
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
- package/resources/models/DESIGN.md +39 -0
- package/resources/models/Models.ts +361 -18
- package/resources/models/TestBackend.ts +56 -1
- package/resources/models/analyticsTable.ts +6 -4
- package/resources/models/backendHelpers.ts +17 -0
- package/resources/models/backendRegistry.ts +76 -21
- package/resources/models/bootstrap.ts +57 -7
- package/resources/models/decideHook.ts +135 -0
- package/resources/models/decision.ts +505 -0
- package/resources/models/decisionStore.ts +419 -0
- package/resources/models/embedHook.ts +272 -55
- package/resources/models/generativeDecision.ts +459 -0
- package/resources/models/types.ts +231 -4
- package/security/DESIGN.md +22 -2
- package/security/authn/oidc/tokenExchange.ts +6 -34
- package/security/authn/oidc/tokenUseTable.ts +27 -0
- package/security/certificateVerification/certificateVerificationSource.ts +2 -3
- package/security/certificateVerification/crlVerification.ts +96 -174
- package/security/certificateVerification/types.ts +0 -3
- package/security/certificateVerification/verificationTables.ts +124 -0
- package/security/certificateVerification/verificationUtils.ts +8 -32
- package/security/jsLoader.ts +9 -1
- package/server/DESIGN.md +27 -1
- package/server/Server.ts +2 -1
- package/server/http.ts +4 -2
- package/server/itc/serverHandlers.js +29 -1
- package/server/jobs/jobOwnership.ts +89 -0
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +15 -4
- package/server/loadRootComponents.js +11 -0
- package/server/serverHelpers/contentTypes.ts +13 -1
- package/server/serverHelpers/serverHandlers.js +11 -2
- package/server/threads/itc.js +32 -6
- package/server/threads/manageThreads.js +268 -122
- package/server/threads/threadServer.js +13 -1
- package/server/threads/windowsProcessTree.ts +409 -0
- package/server/throttle.ts +11 -1
- package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
- package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
- package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
- package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
- package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
- package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
- package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
- package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
- package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
- package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
- package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
- package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
- package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
- package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
- package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
- package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
- package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
- package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
- package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
- package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
- package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
- package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
- package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
- package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
- package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
- package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +61 -54
- package/utility/DESIGN.md +5 -1
- package/utility/errors/hdbError.ts +56 -8
- package/utility/fsync.ts +16 -0
- package/utility/hdbTerms.ts +3 -0
- package/utility/signalling.ts +33 -4
- package/utility/when.ts +22 -0
- package/validation/configValidator.ts +36 -7
package/resources/DESIGN.md
CHANGED
|
@@ -4,8 +4,6 @@ The Resource layer is Harper's universal abstraction: all queryable/mutable thin
|
|
|
4
4
|
|
|
5
5
|
**Read this when:** you're touching the read/write path, authorization, subscriptions, or table CRUD semantics.
|
|
6
6
|
|
|
7
|
-
See also: `../DESIGN.md` for cross-cutting non-obvious internals (RecordObject prototype, `getFromSource` timing, blob orphan cleanup).
|
|
8
|
-
|
|
9
7
|
> **Navigation convention.** This guide references code by **symbol name** (e.g. `_writeUpdate`) and by **section marker** (e.g. `// #section: write-path-internals`). Jump in your editor via go-to-symbol, or `grep` for the section marker. Line numbers drift; symbols and section markers don't.
|
|
10
8
|
|
|
11
9
|
---
|
|
@@ -61,23 +59,23 @@ Static methods are protocol entry points (each wrapped in `transactional()`); in
|
|
|
61
59
|
|
|
62
60
|
One giant `makeTable()` factory that returns a `TableResource extends Resource` class. The file is divided into the sections below; each is anchored by a `// #section: <name>` marker — grep for the marker (or use VS Code's go-to-symbol within the section) to land directly.
|
|
63
61
|
|
|
64
|
-
| Section marker | Contents
|
|
65
|
-
| -------------------------------- |
|
|
66
|
-
| `#section: setup-and-factory` | `makeTable(options)` entry, attribute parsing & primary-key detection, replication wiring, `class Updatable` (RecordObject prototype: `getUpdatedTime`, `getExpiresAt`, `addTo`, `subtractFrom`). Ends where `class TableResource` opens.
|
|
67
|
-
| `#section: static-config` | Static configuration properties: `name`, `primaryStore`, `auditStore`, `primaryKey`, `indices`, `audit`, `databasePath`, `attributes`, `replicate`, `sealed`, `splitSegments`, `getResidencyById`, `dbisDB`, `schemaDefined`, `expirationMS`.
|
|
68
|
-
| `#section: resource-registry` | `sourcedFrom()` (cache/source hierarchy — the largest static), `isCaching`, `shouldRevalidateEvents`, `getResource()`, `_updateResource`, `ensureLoaded()`.
|
|
69
|
-
| `#section: lifecycle-admin` | `getNewId()` (UUID / autoincrement / prefix / time-based strategies), `setTTLExpiration`, residency (`getResidencyRecord`, `setResidency`, `setResidencyById`, `getResidency`), `enableAuditing`, `coerceId`, `dropTable`.
|
|
70
|
-
| `#section: read-path` | `get()` overloads & impl.
|
|
71
|
-
| `#section: authz-hooks` | `allowRead`, `allowUpdate`, `allowCreate`, `allowDelete`.
|
|
72
|
-
| `#section: write-path-public` | `update()`, `save()`, `addTo()`, `subtractFrom()`, `getMetadata`, `getRecord`, `getChanges`, `_setChanges`, `setRecord`, `invalidate()`, `operation()`, `put()`, `create()`, `patch()`.
|
|
73
|
-
| `#section: write-path-internals` | **`_writeUpdate()` — the central write routine** (versioning, conflict resolution, audit, residency, replication metadata, blob orphan tracking). The `write.skipped` flag
|
|
74
|
-
| `isPlainOptions` | _(after the class)_ argument-position helper for `lock()` (distinguishes `lock(options)` from `lock(target, options)`)
|
|
75
|
-
| `#section: search-query` | `search()` (the query engine — index selection, filter evaluation), `transformToOrderedSelect` (select-clause ordering), `transformEntryForSelect` (record → response shape).
|
|
76
|
-
| `#section: pub-sub` | `subscribe()` (subscription request handling, replay, cursor management), `subscribeOnThisThread`, `doesExist()`, `publish()`, `_writePublish()`.
|
|
77
|
-
| `#section: validation` | `validate(record, patch?)` — schema enforcement, computed attributes, attribute coercion.
|
|
78
|
-
| `#section: stats-admin` | `getUpdatedTime`, `addAttributes`, `removeAttributes`, `getSize`, `getAuditSize`, `getStorageStats`, `getRecordCount`, `updatedAttributes` (schema diff machinery).
|
|
79
|
-
| `#section: computed-history` | `setComputedAttribute`, `deleteHistory`, `getHistory` (generator), `getHistoryOfRecord`, `clear`, `cleanup`, `_readTxnForContext`.
|
|
80
|
-
| _(after the class)_ | `getFromSource()` — cache miss → source load (see
|
|
62
|
+
| Section marker | Contents |
|
|
63
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| `#section: setup-and-factory` | `makeTable(options)` entry, attribute parsing & primary-key detection, replication wiring, `class Updatable` (RecordObject prototype: `getUpdatedTime`, `getExpiresAt`, `addTo`, `subtractFrom`). Ends where `class TableResource` opens. |
|
|
65
|
+
| `#section: static-config` | Static configuration properties: `name`, `primaryStore`, `auditStore`, `primaryKey`, `indices`, `audit`, `databasePath`, `attributes`, `replicate`, `sealed`, `splitSegments`, `getResidencyById`, `dbisDB`, `schemaDefined`, `expirationMS`. |
|
|
66
|
+
| `#section: resource-registry` | `sourcedFrom()` (cache/source hierarchy — the largest static), `isCaching`, `shouldRevalidateEvents`, `getResource()`, `_updateResource`, `ensureLoaded()`. |
|
|
67
|
+
| `#section: lifecycle-admin` | `getNewId()` (UUID / autoincrement / prefix / time-based strategies), `setTTLExpiration`, residency (`getResidencyRecord`, `setResidency`, `setResidencyById`, `getResidency`), `enableAuditing`, `coerceId`, `dropTable`. |
|
|
68
|
+
| `#section: read-path` | `get()` overloads & impl. |
|
|
69
|
+
| `#section: authz-hooks` | `allowRead`, `allowUpdate`, `allowCreate`, `allowDelete`. |
|
|
70
|
+
| `#section: write-path-public` | `update()`, `save()`, `addTo()`, `subtractFrom()`, `getMetadata`, `getRecord`, `getChanges`, `_setChanges`, `setRecord`, `invalidate()`, `operation()`, `put()`, `create()`, `patch()`. |
|
|
71
|
+
| `#section: write-path-internals` | **`_writeUpdate()` — the central write routine** (versioning, conflict resolution, audit, residency, replication metadata, blob orphan tracking). The `write.skipped` flag ("Blob orphan cleanup" below) is set in this method's early-return paths. Also `_writeInvalidate`, `_writeRelocate`, `_recordRelocate`, `evict()`, `lock()`/`unlock()` (record locks — see `record-locks.md`), `delete()`, `_writeDelete`. |
|
|
72
|
+
| `isPlainOptions` | _(after the class)_ argument-position helper for `lock()` (distinguishes `lock(options)` from `lock(target, options)`) |
|
|
73
|
+
| `#section: search-query` | `search()` (the query engine — index selection, filter evaluation), `transformToOrderedSelect` (select-clause ordering), `transformEntryForSelect` (record → response shape). |
|
|
74
|
+
| `#section: pub-sub` | `subscribe()` (subscription request handling, replay, cursor management), `subscribeOnThisThread`, `doesExist()`, `publish()`, `_writePublish()`. |
|
|
75
|
+
| `#section: validation` | `validate(record, patch?)` — schema enforcement, computed attributes, attribute coercion. |
|
|
76
|
+
| `#section: stats-admin` | `getUpdatedTime`, `addAttributes`, `removeAttributes`, `getSize`, `getAuditSize`, `getStorageStats`, `getRecordCount`, `updatedAttributes` (schema diff machinery). |
|
|
77
|
+
| `#section: computed-history` | `setComputedAttribute`, `deleteHistory`, `getHistory` (generator), `getHistoryOfRecord`, `clear`, `cleanup`, `_readTxnForContext`. |
|
|
78
|
+
| _(after the class)_ | `getFromSource()` — cache miss → source load (see "getFromSource() timing" below for the resolve-before-commit trap); local helpers (`coerceType`, `isDescendantId`, etc.). |
|
|
81
79
|
|
|
82
80
|
---
|
|
83
81
|
|
|
@@ -94,7 +92,7 @@ One giant `makeTable()` factory that returns a `TableResource extends Resource`
|
|
|
94
92
|
| 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. |
|
|
95
93
|
| 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`). |
|
|
96
94
|
| How are residencies enforced (replication)? | `Table.ts → #section: lifecycle-admin` (residency block: `getResidencyRecord`, `setResidency`, `setResidencyById`, `getResidency`) |
|
|
97
|
-
| How is the RecordObject prototype applied? | `RecordEncoder.ts` (see
|
|
95
|
+
| How is the RecordObject prototype applied? | `RecordEncoder.ts` (see "RecordObject prototype and entryMap" below) |
|
|
98
96
|
| Where is the per-request transaction stored? | `transaction.ts` + `contextStorage` (AsyncLocalStorage) |
|
|
99
97
|
| What is in `context.transaction` when no scope owns one? | Whatever the last table call left there. A completed scope leaves `RELEASED_TRANSACTION` (`DatabaseTransaction.ts`), which reads latest and no-ops on commit. The next table call finds no transaction and `Table.ts → txnForContext` installs an `ImmediateTransaction` in the slot. It reports `open === OPEN`, but `transaction()` and `Resource`'s dispatcher gate on `isJoinableScope` — OPEN _and_ staging its writes for a later commit — so they never join it and start their own scope instead (#2292); `txnForContext` keeps returning it for reads and for writes that reach no wrapper, and those commit per write. It is also the only transaction that opens its native handle inside its own `commit()` (its `getReadTxn` never opens one), which is why `commit()` re-reads `this.transaction` after its save loop (#2288). |
|
|
100
98
|
| How do I find a transaction that is holding write intents and is never reaped? | Three log surfaces, joined by the native transaction id (`longLivedTransactions.ts`, #2471). `runLongLivedTransactionSweep()` runs on the **main thread only** — rocksdb-js's registry is process-global, so one sweep sees every worker's handles — and names any handle open past `storage.longTransactionReportThreshold` (default `5m`, `0` disables all three), including handles no `DatabaseTransaction` owns. Changing the threshold clears the accrued backoff, so a handle already under observation is re-measured against the new value. `reportIfLongLived()` in each worker's monitor reports every link holding its own handle under that link's own id, adding `startedFrom`, the table, and the state keeping it alive (`source-apply`, `replay`, `commit-phase`, `active` when written in the current or preceding monitor tick), which is what the reap branches below it deliberately do not do. Attribution lines are capped at 10 per monitor tick; a summary names deferred holders, whose due backoff remains intact for a later tick. Attribution suppression is keyed on database path, native id, and handle-open time, so a reattached descriptor id cannot inherit its predecessor's backoff. The two stuck-commit logs (`checkOverloaded()` and `abandonCommitAfterDeadline()`) append holder candidates from **every** database, the commit's own first and then by age — the verification table is one process-global slot array, so a holder in another database parks this commit at the same rate — never filtered by age, because a coordinated retry can park on a transaction younger than itself. |
|
|
@@ -209,13 +207,12 @@ first and a throw from it is what stops the prune.
|
|
|
209
207
|
| `Table.deleteHistory` | LMDB (`RocksTransactionLogStore.remove()` is a no-op, so it must NOT raise) |
|
|
210
208
|
| `delete_transaction_logs_before` whole-database branch (`ResourceBridge.ts`) | RocksDB |
|
|
211
209
|
|
|
212
|
-
|
|
210
|
+
Things that are easy to get wrong here:
|
|
213
211
|
|
|
214
212
|
- **The floor cannot be derived from the surviving log.** For four of the five paths the oldest
|
|
215
213
|
surviving entry would do, because they prune a database-wide time prefix. `Table.deleteHistory`
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
surviving `…143.309` — a log-derived floor would have certified a cursor at `…145`.
|
|
214
|
+
removes one table's entries from a database-scoped log, so a sibling's entry survives _below_ the
|
|
215
|
+
newest entry it removed, and a floor taken from that survivor certifies cursors over removed history.
|
|
219
216
|
- **The record's presence is the trust marker.** `Symbol.for('audit-floor')` is a different key from
|
|
220
217
|
`last-removed`, which is still live and still maintained by the LMDB retention loop (#2338 hardened
|
|
221
218
|
its write path and added tests for the retry-carry — do not remove it). They coexist because they
|
|
@@ -368,7 +365,7 @@ Tests: `../unitTests/resources/defineResource.test.js`, `../unitTests/resources/
|
|
|
368
365
|
- **The override makes the access-control decision.** No `allow*` predicate runs for it. An override that is not meant to be public must check authorization itself.
|
|
369
366
|
- **FK-side relationship accessors (`relationship.from`) are a synchronous contract** (v4 parity). The resolvers built in `Table.updatedAttributes` must read through `getSync`/`getEntry` (which block on a RocksDB block-cache miss, like v4's LMDB page faults) — never `get()`, whose `async: true` path returns a Promise on a cache miss and would leak an intermittent value-or-Promise to `record.<relation>` in user code. Pinned by the "relationship property access is synchronous" test in `../unitTests/resources/query.test.js`, which stubs `store.get` to throw.
|
|
370
367
|
- **Known gap, not covered by that test:** the `.to` side (one-to-many/many-to-many, resolved via `relatedTable.search(...).asArray`) is synchronous for a local read, but `asArray` can still hand back a Promise when the search lands on a **caching** table whose row needs source revalidation — `transformEntryForSelect` returns `loadingFromSource.then(transform)` for an expired/invalidated entry. Same bug class this PR closes on the FK side; not yet fixed here.
|
|
371
|
-
- When adding a new early-return path inside a commit handler in `_writeUpdate`, follow the blob-cleanup protocol
|
|
368
|
+
- When adding a new early-return path inside a commit handler in `_writeUpdate`, follow the blob-cleanup protocol in "Blob orphan cleanup" below.
|
|
372
369
|
- If you add a new top-level section to `Table.ts`, drop a `// #section: <name>` marker at its start and add a row to the section map above.
|
|
373
370
|
- Tests for this layer live in `../unitTests/resources/`.
|
|
374
371
|
|
|
@@ -564,28 +561,15 @@ Without this, every re-delivery appended another entry under the origin's log ke
|
|
|
564
561
|
|
|
565
562
|
## A second sequential save() on the same ImmediateTransaction context must chain on `operation.innerCommit`
|
|
566
563
|
|
|
567
|
-
Two `update()`+`save()` cycles on
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
`this.save(operation, ...)` for every write it processes — that's fine when the write commits inline,
|
|
577
|
-
but this reused-context write's real work happens in a _third_, more deeply nested `commit()` call
|
|
578
|
-
(triggered by `immediateCommit` inside the nested `save()`), whose promise never propagates back through
|
|
579
|
-
any of the enclosing calls. `Table.save()`'s ordinary `#savingOperation` path used to just return
|
|
580
|
-
`#saveOperation(operation)`'s result directly — which can resolve before that nested native commit
|
|
581
|
-
actually settles, so a caller's `await resource.save()` can return before the write is durable (a real,
|
|
582
|
-
if narrow, race: `LockTest.get()` immediately after can read the pre-write value). The lock-writable hold
|
|
583
|
-
branch already avoided this by explicitly returning `operation.innerCommit` after its own recursive
|
|
584
|
-
`save()`; the ordinary path now does the same — `when(this.#saveOperation(operation), () =>
|
|
585
|
-
operation.innerCommit)`. `operation.innerCommit` is `undefined` when a write commits inline (no
|
|
586
|
-
immediateCommit), so this is safe for the common case. Found via record-lock scoped-lock staging
|
|
587
|
-
(harper#483), which is what first made this reused-closed-context pattern reachable for an ordinary
|
|
588
|
-
resource, but the gap is general to `Table.save()`, not lock-specific.
|
|
564
|
+
Two `update()`+`save()` cycles on one resource instance outside `transaction()` reuse one
|
|
565
|
+
`ImmediateTransaction` after its first cycle has closed it. The second write then commits through a
|
|
566
|
+
nested `commit()` on a fresh native transaction (the `immediateCommit` branch of
|
|
567
|
+
`DatabaseTransaction.save()`), whose promise the outer commit's sweep loop discards; that branch
|
|
568
|
+
stashes it on `operation.innerCommit`, which is `undefined` for a write that commits inline. Both
|
|
569
|
+
`Table.save()` branches — the ordinary `#savingOperation` path and the lock-writable hold path — wait
|
|
570
|
+
on it, or `await resource.save()` returns before the write is durable and an immediate read sees the
|
|
571
|
+
old value. The gap is general to `Table.save()`, not lock-specific. Exercised by `recordLock.test.js`
|
|
572
|
+
"scoped lock outside transaction() persists across sequential saves until unlock()".
|
|
589
573
|
|
|
590
574
|
## A commit retry re-saves into the transaction it is retrying; `ImmediateTransaction.save()` must forward it
|
|
591
575
|
|
|
@@ -704,7 +688,7 @@ The additive-only rule above repairs the _consumer_ of a partial peer snapshot;
|
|
|
704
688
|
|
|
705
689
|
## Subscription version selection (`Table.ts`)
|
|
706
690
|
|
|
707
|
-
`Table.subscribe` resolves audit events through `eventFromAudit` for both replay and live delivery: unless `includeSuperseded` (defaulting to `rawEvents`) is enabled, record mutations must match the current primary version; an absent primary entry cannot establish a current version and is skipped. Publishing advances that primary version too: a put followed by a publish replays the message but not the put under the default rule. Messages and control events are independent. Version filtering precedes historical reconstruction and `previousCount` acceptance, while audit cursors advance over rejected entries. Single-record replay retains its bounded history walk because it also contains independent published messages; only an accepted replay event suppresses the current snapshot, and `eventFilter` also applies to that fallback. Default replay reads the current primary entry for each mutation. Opting into superseded full records retains audit-history reconstruction costs and retention limits, including patch reconstruction on durable MQTT live delivery.
|
|
691
|
+
`Table.subscribe` resolves audit events through `eventFromAudit` for both replay and live delivery: unless `includeSuperseded` (defaulting to `rawEvents`) is enabled, record mutations must match the current primary version; an absent primary entry cannot establish a current version and is skipped. Publishing advances that primary version too: a put followed by a publish replays the message but not the put under the default rule. Messages and control events are independent. Version filtering precedes historical reconstruction and `previousCount` acceptance, while audit cursors advance over rejected entries. Single-record replay retains its bounded history walk because it also contains independent published messages; only an accepted replay event suppresses the current snapshot, and `eventFilter` also applies to that fallback. Default replay reads the current primary entry for each mutation. Opting into superseded full records retains audit-history reconstruction costs and retention limits, including patch reconstruction on durable MQTT live delivery. Collection `startTime` replay leaves `subscription.startTime` — the exclusive resume cursor `DurableSubscriptionsSession.saveSubscriptions` persists — never past a key with an unhandled in-table record: RocksDB gives every record of a transaction one `txnLogKey`, so the cursor moves to a key only on reaching the next key or finishing, and an early return (failed send, close at a yield or drain) leaves it up to one transaction back, which the resume re-sends (`subscriptionReplay.test.js`, "cursor after replay stops early"). The cursor counts events handed to the subscription queue, not consumed ones; `close()` discards the queue.
|
|
708
692
|
|
|
709
693
|
## Audit-store `'committed'` notification batching (`transactionBroadcast.ts`)
|
|
710
694
|
|
|
@@ -718,20 +702,27 @@ The cross-thread subscription path (default `crossThreads`) drives every `Table.
|
|
|
718
702
|
## Audit-entry removal loops must track every `removeAuditEntry()`/`removeEntry()` promise
|
|
719
703
|
|
|
720
704
|
`scheduleAuditCleanup` (`auditStore.ts`) and `Table.deleteHistory` (`Table.ts`, the LMDB path behind
|
|
721
|
-
`delete_transaction_logs_before`) both iterate a range of audit records and remove each one.
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
later as an unhandled rejection
|
|
726
|
-
|
|
727
|
-
and drain all tracked promises before returning — never stash a per-iteration promise in an outer
|
|
728
|
-
variable to await only the last one. `Table.deleteHistory` allows up to 1,000 LMDB removals in flight
|
|
705
|
+
`delete_transaction_logs_before`) both iterate a range of audit records and remove each one. Any loop
|
|
706
|
+
that removes audit/primary-store entries in a batch must attach a rejection handler to every removal
|
|
707
|
+
immediately and drain all tracked promises before returning — never stash a per-iteration promise in
|
|
708
|
+
an outer variable to await only the last one: an overwritten promise's rejection is never awaited or
|
|
709
|
+
caught, and surfaces later as an unhandled rejection with no log to explain it. `Table.deleteHistory`
|
|
710
|
+
allows up to 1,000 LMDB removals in flight
|
|
729
711
|
(ten for RocksDB) so storage writes batch without growing an unbounded pending
|
|
730
712
|
set. Live removals are tracked in a `Set`, and each one removes itself and wakes at most one parked
|
|
731
713
|
producer when it settles, so any completion releases the loop. In these removal loops, do not repeatedly
|
|
732
714
|
race the live set: each race attaches another reaction to every long-pending removal. Both phases drain
|
|
733
715
|
their tracked removals before settling, including when iteration throws. `scheduleAuditCleanup` remains
|
|
734
|
-
sequential because it is an automatic background loop
|
|
716
|
+
sequential because it is an automatic background loop, and it ends a pass at its first failed removal
|
|
717
|
+
rather than continuing past it: its `deleted` count (the backoff input) and the `last-removed` marker it
|
|
718
|
+
writes then cover a contiguous removed prefix — nothing above that marker was removed by the pass — and
|
|
719
|
+
the next pass retries the failed entry first. Past a failure there is no key the marker can truthfully
|
|
720
|
+
record, and counting failures as progress re-armed an all-failing pass at 10 ms. The guarantee is
|
|
721
|
+
pass-local: the marker is written after its removals, `deleteHistory` never writes it, and a marker an
|
|
722
|
+
older build persisted past a failed entry is not repaired; completeness is the audit floor's question.
|
|
723
|
+
Stopping costs liveness only for an entry that fails on every pass, which is why `removeAuditEntry` must
|
|
724
|
+
fail only when `auditStore.remove()` does (below). Regression: `auditLog.test.js` "ends a pass at a failed
|
|
725
|
+
removal".
|
|
735
726
|
|
|
736
727
|
Individual removal failures are logged and excluded from the returned count, but a purge that attempted
|
|
737
728
|
at least one removal and completed none rejects with the first error after both phases have drained.
|
|
@@ -749,9 +740,11 @@ followed by an unconditional remove: a record recreated between those operations
|
|
|
749
740
|
`removeAuditEntry` has a second, nested version of the same hazard: for a `'delete'`-type audit record it
|
|
750
741
|
also invokes a per-table delete callback (`addDeleteRemovalCallback`) that removes the corresponding
|
|
751
742
|
primary-store tombstone. That callback's promise must be returned and joined with the audit-store
|
|
752
|
-
removal (currently via `Promise.all`, with the callback's own rejection
|
|
753
|
-
|
|
754
|
-
tombstone
|
|
743
|
+
removal (currently via `Promise.all`, with the callback's own rejection — and a throwing tombstone
|
|
744
|
+
lookup, such as lmdb-js `getEntry(undefined)` for an undecodable recordId — caught and logged through
|
|
745
|
+
`warnContained`, so neither a failed tombstone cleanup nor a throwing log sink gets misreported as a failed
|
|
746
|
+
audit-entry removal) — otherwise the tombstone removal is fire-and-forget and the same detached-rejection
|
|
747
|
+
hazard reappears one level down.
|
|
755
748
|
A tombstone whose cleanup fails this way is not swept automatically — `scheduleAuditCleanup`'s automatic
|
|
756
749
|
pass never retries it, since the audit entry that would have triggered a retry is already gone. It sits
|
|
757
750
|
in the primary store until an operator runs `delete_transaction_logs_before` with `cleanup_deleted_records: true`.
|
|
@@ -779,11 +772,9 @@ of the exported `scheduleAuditCleanup`, because a store-wide segment purge loopi
|
|
|
779
772
|
duplicated work. If a future change passes `skipThreadCheck: true` at the registration site, that
|
|
780
773
|
backstop — not the registration — becomes the thing keeping the loop single.
|
|
781
774
|
|
|
782
|
-
Both re-arm guards are **Rocks-only
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
behaviours, not invariants this section establishes — don't read the paragraphs above as
|
|
786
|
-
engine-independent.
|
|
775
|
+
Both re-arm guards are **Rocks-only**. The LMDB arm re-arms unconditionally, so it neither yields to
|
|
776
|
+
an already-pending pass (a pressure-armed 100ms pass can be cancelled and replaced by the idle
|
|
777
|
+
backoff) nor restricts itself to one worker — pre-existing LMDB behavior, not an invariant.
|
|
787
778
|
|
|
788
779
|
Two things a purge does **not** need to coordinate, both load-bearing for the continuous cadence.
|
|
789
780
|
Unlinking a segment a consumer has mapped is safe **on POSIX**: the inode outlives the unlink, and the
|
|
@@ -808,11 +799,11 @@ number into its write instruction synchronously and the native writer consumes i
|
|
|
808
799
|
(`node_modules/lmdb/write.js`), so a pass suspended inside `await removeAuditEntry()` still has a
|
|
809
800
|
delete pending against the primary and audit DBIs, and LMDB forbids closing a DBI an existing
|
|
810
801
|
transaction has modified. `dropDatabase()` and the legacy arm of `Table.dropTable()` await it.
|
|
811
|
-
`closeDatabase()` and branch `close()`
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
802
|
+
`closeDatabase()` and branch `close()` close commit admission, then drain tracked Rocks transactions,
|
|
803
|
+
table maintenance, audit cleanup, and derived-index work before closing stores. Every environment
|
|
804
|
+
touch remaining in a resumed cleanup pass — cursor advance, cursor release, marker write, re-arm —
|
|
805
|
+
also re-checks `rootStore.status`. `resetDatabases()` closes LMDB roots with no retirement call at all,
|
|
806
|
+
so that re-check is a routine
|
|
816
807
|
path rather than a defensive one.
|
|
817
808
|
|
|
818
809
|
The last-removed marker is retained until it commits. A rejected write is logged and carried to the
|
|
@@ -824,13 +815,7 @@ were already removed.
|
|
|
824
815
|
|
|
825
816
|
When a blob attribute is created from a Node `Readable` (e.g. `createBlob(stream)` then `row.payload_blob = blob; await table.put(row)`), the put does **not** wait for the underlying stream to fully drain into the file before resolving. Internally `saveBlob` kicks off a `writeBlobWithStream` pipeline whose `storageInfo.saving` promise is tracked separately. The put resolves once encoding has captured the blob reference; the bytes finish writing concurrently.
|
|
826
817
|
|
|
827
|
-
Consequence for callers that wrap the source in a hashing `Transform`: calling `hash.digest('hex')` after `await table.put()` is unsafe — more `chunk.update()` calls can still fire as the stream drains, producing `Error [ERR_CRYPTO_HASH_FINALIZED]: Digest already called`.
|
|
828
|
-
|
|
829
|
-
- Buffer first, then hash + put (what `components/deploymentRecorder.ts` does for Slice A — small payloads only).
|
|
830
|
-
- Hash via Transform while extraction reads the stream, and only finalize the hash on the Transform's `'end'` event before any second put with the final hash.
|
|
831
|
-
- Await `storageInfo.saving` directly if you have a handle to the FileBackedBlob (the cleanest path for streaming).
|
|
832
|
-
|
|
833
|
-
Future agents touching `components/deploymentRecorder.ts` for Slice B's streaming variant should pick one of the latter two patterns.
|
|
818
|
+
Consequence for callers that wrap the source in a hashing `Transform`: calling `hash.digest('hex')` after `await table.put()` is unsafe — more `chunk.update()` calls can still fire as the stream drains, producing `Error [ERR_CRYPTO_HASH_FINALIZED]: Digest already called`. Finalize the hash only after the source has ended and the blob's `storageInfo.saving` has settled, as `components/deploymentRecorder.ts`'s streaming ingest does (`await Promise.all([putDone, tapDone, saving])` before `digest`). Its in-memory sources, and its capped fallback for a missing deployment table, buffer and hash before the put instead.
|
|
834
819
|
|
|
835
820
|
## Table drops, the `dropping` tombstone, and ghost tables
|
|
836
821
|
|
|
@@ -847,29 +832,24 @@ silently re-opened with create-if-missing on the next start, which resurrects "d
|
|
|
847
832
|
|
|
848
833
|
## The exclusive `update-attributes` lock is a bounded synchronous wait, and drop-then-recreate needs the column-family eviction fix (`Table.ts`)
|
|
849
834
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
`
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
by-name registry entry alive whenever other worker threads hold handles, so the recreate
|
|
869
|
-
silently reuses a dangling handle and every write fails with "Invalid column family specified
|
|
870
|
-
in write batch", poisoning the whole database env until restart. The regression suite for all
|
|
871
|
-
of this is `unitTests/resources/dropTableGhost.test.js` (it fails by design on pre-fix
|
|
872
|
-
bindings).
|
|
835
|
+
The create/schema-update path's exclusive `update-attributes` lock is a synchronous bounded wait
|
|
836
|
+
(`acquireUpdateAttributesLock` in `Table.ts`: brief hot spin, then `Atomics.wait` backoff, retryable
|
|
837
|
+
`ServerError` after the 10s `UPDATE_ATTRIBUTES_LOCK_TIMEOUT`, harper#2251). Release is structural —
|
|
838
|
+
`table()` releases in a single `finally` and `dropTable` uses `withUpdateAttributesLock` — so a throw
|
|
839
|
+
inside the locked window cannot leak the lock (`unitTests/resources/updateAttributesLock.test.js`).
|
|
840
|
+
Because the acquire can throw, `table()` takes the RocksDB lock _before_ it mutates the live `Table`
|
|
841
|
+
(attributes, class metadata, index handles): losing the race then leaves this worker's in-memory
|
|
842
|
+
schema exactly as it found it, and moving any mutation above that acquire reintroduces schema drift
|
|
843
|
+
the catalog never saw. LMDB keeps the lazy acquire — its `exclusiveLock()` is an environment-wide
|
|
844
|
+
write transaction that cannot time out, so taking it eagerly would stall every write to the database
|
|
845
|
+
on an unchanged reload. A successful acquire that waited past `UPDATE_ATTRIBUTES_LOCK_SLOW_WAIT` (1s)
|
|
846
|
+
warns once, since contention is otherwise invisible until it becomes a timeout. The locked sections
|
|
847
|
+
MUST stay synchronous: the wait blocks the event loop, so an awaited operation inside one would stall
|
|
848
|
+
a concurrent acquirer to its deadline. Dropping then recreating a same-named table within one process
|
|
849
|
+
depends on rocksdb-js's column-family eviction fix (≥ 2.1.0, rocksdb-js#647; `package.json` pins a
|
|
850
|
+
later release): without it the recreate reuses a dangling handle and every write fails with "Invalid
|
|
851
|
+
column family specified in write batch". Regression suite: `unitTests/resources/dropTableGhost.test.js`,
|
|
852
|
+
which fails by design on pre-fix bindings.
|
|
873
853
|
|
|
874
854
|
## RocksDB transaction log purges are database-wide only (`ResourceBridge.deleteTransactionLogsBefore`)
|
|
875
855
|
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { cleanupUnusedBlobs, collectRetainedFileIds } from './blob.ts';
|
|
2
2
|
import { Transaction as LMDBTransaction } from 'lmdb';
|
|
3
3
|
import { getNextMonotonicTime } from '../utility/lmdb/commonUtility.ts';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DatabaseClosingError,
|
|
6
|
+
DatabaseDrainTimeoutError,
|
|
7
|
+
ServerError,
|
|
8
|
+
TransactionCommitConflictTimeoutError,
|
|
9
|
+
} from '../utility/errors/hdbError.ts';
|
|
5
10
|
import { lockNotHeldError, type RecordLockHandle } from './recordLock.ts';
|
|
6
11
|
import * as harperLogger from '../utility/logging/harper_logger.ts';
|
|
7
12
|
import type { Context, Id } from './ResourceInterface.ts';
|
|
8
13
|
import * as envMngr from '../utility/environment/environmentManager.ts';
|
|
9
14
|
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
|
|
10
15
|
import { convertToMS } from '../utility/common_utils.ts';
|
|
11
|
-
import { when } from '../utility/when.ts';
|
|
16
|
+
import { settleBeforeDeadline, when } from '../utility/when.ts';
|
|
12
17
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
13
18
|
import { Transaction as RocksTransaction, type Store as RocksStore, constants } from '@harperfast/rocksdb-js';
|
|
14
19
|
const RETRY_NOW_VALUE = constants.RETRY_NOW_VALUE;
|
|
@@ -65,6 +70,7 @@ interface OutstandingCommit {
|
|
|
65
70
|
start: number;
|
|
66
71
|
prev: OutstandingCommit | undefined;
|
|
67
72
|
next: OutstandingCommit | undefined;
|
|
73
|
+
commitResolution: Promise<number | void>;
|
|
68
74
|
// Identity for the one-time checkOverloaded() log below (harper#2001) — otherwise a stuck commit
|
|
69
75
|
// gives no indication of which database/table/resource to investigate. Snapshotted at arm time
|
|
70
76
|
// (not read from `this.writes`/`this.startedFrom` lazily off the DatabaseTransaction object)
|
|
@@ -74,6 +80,7 @@ interface OutstandingCommit {
|
|
|
74
80
|
// so that every outstanding commit carries its own identity and its own `logged` flag: a second
|
|
75
81
|
// commit that is still stuck once the first settles becomes the new oldest and logs on its own.
|
|
76
82
|
store: any;
|
|
83
|
+
rootStore: any;
|
|
77
84
|
startedFrom: { resourceName: string; method: string } | undefined;
|
|
78
85
|
nativeTransaction: any;
|
|
79
86
|
logged: boolean;
|
|
@@ -81,6 +88,10 @@ interface OutstandingCommit {
|
|
|
81
88
|
let oldestOutstandingCommit: OutstandingCommit | undefined;
|
|
82
89
|
let newestOutstandingCommit: OutstandingCommit | undefined;
|
|
83
90
|
let outstandingCommitCount = 0;
|
|
91
|
+
const suspendedDatabaseCommits = new WeakMap<object, number>();
|
|
92
|
+
const permanentlySuspended = Symbol('permanentlySuspendedDatabaseCommits');
|
|
93
|
+
let suspendedDatabaseRootCount = 0;
|
|
94
|
+
let hasPermanentlySuspendedDatabaseRoots = false;
|
|
84
95
|
// Caps the stuck-commit log (checkOverloaded() below) to at most one line per this interval across
|
|
85
96
|
// the whole thread, regardless of how many distinct commits individually cross the threshold — see
|
|
86
97
|
// the comment at the log site for why a per-commit-only dedup isn't enough under sustained overload.
|
|
@@ -101,11 +112,12 @@ function allowStuckCommitLog(site: 'shed' | 'abandon', now: number): boolean {
|
|
|
101
112
|
function describeCommitIdentity(
|
|
102
113
|
store: any,
|
|
103
114
|
startedFrom: { resourceName: string; method: string } | undefined,
|
|
104
|
-
nativeTransaction: any
|
|
115
|
+
nativeTransaction: any,
|
|
116
|
+
rootStore = store?.rootStore
|
|
105
117
|
): string {
|
|
106
118
|
const nativeTransactionId = nativeTransaction?.id;
|
|
107
119
|
return (
|
|
108
|
-
`from table: ${
|
|
120
|
+
`from table: ${rootStore?.databaseName ?? '?'}.${store?.name ?? '?'}` +
|
|
109
121
|
(nativeTransactionId !== undefined ? ` (transaction ${nativeTransactionId})` : '') +
|
|
110
122
|
(startedFrom?.resourceName
|
|
111
123
|
? `, started from ${startedFrom.resourceName}${startedFrom.method ? '.' + startedFrom.method : ''}`
|
|
@@ -121,9 +133,9 @@ function describeCommitIdentity(
|
|
|
121
133
|
// from its own submission, keeping the overload window per-attempt rather than cumulative over a
|
|
122
134
|
// retry ladder. `.then(untrack, untrack)` also marks an ERR_BUSY rejection handled, so this
|
|
123
135
|
// tracking never surfaces as an unhandled rejection alongside the caller's own handler.
|
|
124
|
-
// Exported
|
|
125
|
-
//
|
|
126
|
-
//
|
|
136
|
+
// Exported so raw RocksDB writers and unit tests share the same queue. The unlink order that matters
|
|
137
|
+
// (a middle or tail node settling first) cannot be forced through real writes, and a node left linked
|
|
138
|
+
// would 503 every write on this thread forever.
|
|
127
139
|
// Also the single source for write-queue-depth accounting (getTransactionQueueDepths below): every
|
|
128
140
|
// native commit this function tracks is, by definition, exactly the write-queue backlog — see the
|
|
129
141
|
// comment there for why that used to be a second, separately-maintained counter.
|
|
@@ -133,7 +145,8 @@ export function trackOutstandingCommit(
|
|
|
133
145
|
commitResolution: Promise<number | void>,
|
|
134
146
|
store?: any,
|
|
135
147
|
startedFrom?: { resourceName: string; method: string },
|
|
136
|
-
nativeTransaction?: any
|
|
148
|
+
nativeTransaction?: any,
|
|
149
|
+
rootStore = store?.rootStore
|
|
137
150
|
): void {
|
|
138
151
|
// Guards against a future caller passing a non-Promise: today commit() always hands this a real
|
|
139
152
|
// Promise, but an unguarded link here would leave a node permanently wedged in the list (503ing
|
|
@@ -143,7 +156,9 @@ export function trackOutstandingCommit(
|
|
|
143
156
|
start: performance.now(),
|
|
144
157
|
prev: newestOutstandingCommit,
|
|
145
158
|
next: undefined,
|
|
159
|
+
commitResolution,
|
|
146
160
|
store,
|
|
161
|
+
rootStore,
|
|
147
162
|
startedFrom,
|
|
148
163
|
nativeTransaction,
|
|
149
164
|
logged: false,
|
|
@@ -172,6 +187,120 @@ export function trackOutstandingCommit(
|
|
|
172
187
|
commitResolution.then(untrack, untrack);
|
|
173
188
|
}
|
|
174
189
|
|
|
190
|
+
export function databaseCommitsSuspended(rootStore: object | undefined): boolean {
|
|
191
|
+
if (rootStore == null) return false;
|
|
192
|
+
if ((rootStore as any).status === 'closed') return true;
|
|
193
|
+
if (suspendedDatabaseRootCount === 0 && !hasPermanentlySuspendedDatabaseRoots) return false;
|
|
194
|
+
if ((rootStore as any)[permanentlySuspended]) return true;
|
|
195
|
+
return suspendedDatabaseRootCount > 0 && (suspendedDatabaseCommits.get(rootStore) ?? 0) > 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Keep abandoned root wrappers fenced without retaining the process-wide active-fence counter. */
|
|
199
|
+
export function permanentlySuspendDatabaseCommits(rootStores: Iterable<object>): void {
|
|
200
|
+
for (const rootStore of rootStores) {
|
|
201
|
+
if ((rootStore as any)[permanentlySuspended]) continue;
|
|
202
|
+
const suspension = suspendedDatabaseCommits.get(rootStore) ?? 0;
|
|
203
|
+
if (suspension > 0) suspendedDatabaseRootCount--;
|
|
204
|
+
suspendedDatabaseCommits.delete(rootStore);
|
|
205
|
+
(rootStore as any)[permanentlySuspended] = true;
|
|
206
|
+
hasPermanentlySuspendedDatabaseRoots = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function getSuspendedDatabaseRootCount(): number {
|
|
211
|
+
return suspendedDatabaseRootCount;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function commitTrackedRocksTransaction(
|
|
215
|
+
transaction: RocksTransaction,
|
|
216
|
+
store: any,
|
|
217
|
+
rootStore = store?.rootStore
|
|
218
|
+
): Promise<number | void> {
|
|
219
|
+
if (databaseCommitsSuspended(rootStore)) {
|
|
220
|
+
try {
|
|
221
|
+
transaction.abort();
|
|
222
|
+
} catch {}
|
|
223
|
+
return Promise.reject(new DatabaseClosingError(rootStore?.databaseName ?? rootStore?.path ?? 'unknown'));
|
|
224
|
+
}
|
|
225
|
+
let commitResolution: Promise<number | void>;
|
|
226
|
+
try {
|
|
227
|
+
commitResolution = transaction.commit() as Promise<number | void>;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return Promise.reject(error);
|
|
230
|
+
}
|
|
231
|
+
trackOutstandingCommit(commitResolution, store, undefined, transaction, rootStore);
|
|
232
|
+
return commitResolution;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let databaseCommitDrainTimeoutMilliseconds = 120_000;
|
|
236
|
+
|
|
237
|
+
export function getDatabaseCommitDrainTimeoutMilliseconds(): number {
|
|
238
|
+
return databaseCommitDrainTimeoutMilliseconds;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function setDatabaseCommitDrainTimeoutMilliseconds(timeoutMilliseconds: number): number {
|
|
242
|
+
const previous = databaseCommitDrainTimeoutMilliseconds;
|
|
243
|
+
databaseCommitDrainTimeoutMilliseconds = timeoutMilliseconds;
|
|
244
|
+
return previous;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Stop new native commits for these database roots and wait for submissions already in flight.
|
|
249
|
+
* The returned release is reference-counted and idempotent so overlapping lifecycle operations
|
|
250
|
+
* cannot reopen commit admission underneath each other.
|
|
251
|
+
*/
|
|
252
|
+
export function suspendDatabaseCommits(rootStores: Iterable<object>): {
|
|
253
|
+
waitForDrain(options?: { deadline?: number; databaseName?: string }): Promise<void>;
|
|
254
|
+
release(): void;
|
|
255
|
+
} {
|
|
256
|
+
const roots = [...new Set(rootStores)];
|
|
257
|
+
const rootSet = new Set(roots);
|
|
258
|
+
for (const rootStore of roots) {
|
|
259
|
+
if ((rootStore as any)[permanentlySuspended]) continue;
|
|
260
|
+
const count = suspendedDatabaseCommits.get(rootStore) ?? 0;
|
|
261
|
+
if (count === 0) suspendedDatabaseRootCount++;
|
|
262
|
+
suspendedDatabaseCommits.set(rootStore, count + 1);
|
|
263
|
+
}
|
|
264
|
+
let released = false;
|
|
265
|
+
return {
|
|
266
|
+
async waitForDrain(options = {}) {
|
|
267
|
+
const timeoutMilliseconds =
|
|
268
|
+
options.deadline === undefined
|
|
269
|
+
? databaseCommitDrainTimeoutMilliseconds
|
|
270
|
+
: Math.max(0, options.deadline - Date.now());
|
|
271
|
+
const deadline = options.deadline ?? Date.now() + timeoutMilliseconds;
|
|
272
|
+
for (;;) {
|
|
273
|
+
const pending: Promise<number | void>[] = [];
|
|
274
|
+
for (let outstanding = oldestOutstandingCommit; outstanding; outstanding = outstanding.next) {
|
|
275
|
+
if (rootSet.has(outstanding.rootStore)) pending.push(outstanding.commitResolution);
|
|
276
|
+
}
|
|
277
|
+
if (pending.length === 0) return;
|
|
278
|
+
await settleBeforeDeadline(
|
|
279
|
+
pending,
|
|
280
|
+
deadline,
|
|
281
|
+
() =>
|
|
282
|
+
new DatabaseDrainTimeoutError(
|
|
283
|
+
options.databaseName ?? (roots[0] as any)?.databaseName ?? (roots[0] as any)?.path ?? 'unknown',
|
|
284
|
+
timeoutMilliseconds
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
release() {
|
|
290
|
+
if (released) return;
|
|
291
|
+
released = true;
|
|
292
|
+
for (const rootStore of roots) {
|
|
293
|
+
if ((rootStore as any)[permanentlySuspended]) continue;
|
|
294
|
+
const count = suspendedDatabaseCommits.get(rootStore) ?? 0;
|
|
295
|
+
if (count <= 1) {
|
|
296
|
+
suspendedDatabaseCommits.delete(rootStore);
|
|
297
|
+
suspendedDatabaseRootCount--;
|
|
298
|
+
} else suspendedDatabaseCommits.set(rootStore, count - 1);
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
175
304
|
/**
|
|
176
305
|
* How many write commits are outstanding on this thread and how long the oldest has been waiting
|
|
177
306
|
* (`oldestAgeMs` is undefined when none is). `oldestAgeMs` is exactly the value checkOverloaded()
|
|
@@ -1027,11 +1156,12 @@ export class DatabaseTransaction implements Transaction {
|
|
|
1027
1156
|
describeCommitIdentity(
|
|
1028
1157
|
oldestOutstandingCommit.store,
|
|
1029
1158
|
oldestOutstandingCommit.startedFrom,
|
|
1030
|
-
oldestOutstandingCommit.nativeTransaction
|
|
1159
|
+
oldestOutstandingCommit.nativeTransaction,
|
|
1160
|
+
oldestOutstandingCommit.rootStore
|
|
1031
1161
|
) +
|
|
1032
1162
|
`.` +
|
|
1033
1163
|
describeHolderCandidates(
|
|
1034
|
-
oldestOutstandingCommit.
|
|
1164
|
+
oldestOutstandingCommit.rootStore?.path,
|
|
1035
1165
|
oldestOutstandingCommit.nativeTransaction?.id
|
|
1036
1166
|
) +
|
|
1037
1167
|
` Further record updates and publishes from new application requests on this thread ` +
|
|
@@ -1398,6 +1528,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
1398
1528
|
}
|
|
1399
1529
|
// with options.transaction set this is a retry round — the save loop above already
|
|
1400
1530
|
// re-staged the writes into it
|
|
1531
|
+
this.assertDatabaseCommitAllowed(transaction);
|
|
1401
1532
|
commitResolution = transaction.commit() as Promise<void>;
|
|
1402
1533
|
recordCommitLatency(commitResolution, performance.now());
|
|
1403
1534
|
// Write-queue-depth accounting for this replay commit happens uniformly below, via
|
|
@@ -1457,6 +1588,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
1457
1588
|
// getReadTxn), so commit() can resolve to RETRY_NOW_VALUE. That
|
|
1458
1589
|
// sentinel (a number) is why commitResolution is typed
|
|
1459
1590
|
// Promise<number | void>; it is handled in the resolve callback below.
|
|
1591
|
+
this.assertDatabaseCommitAllowed(transaction);
|
|
1460
1592
|
commitResolution = transaction.commit();
|
|
1461
1593
|
// Record how long this commit stays outstanding (submit → settle) as a distribution
|
|
1462
1594
|
// metric. This is the same clock the overload check uses (trackOutstandingCommit
|
|
@@ -1795,6 +1927,20 @@ export class DatabaseTransaction implements Transaction {
|
|
|
1795
1927
|
this.writesAbandoned = false;
|
|
1796
1928
|
}
|
|
1797
1929
|
|
|
1930
|
+
private assertDatabaseCommitAllowed(transaction: RocksTransaction): void {
|
|
1931
|
+
const rootStore = this.writes[0]?.store?.rootStore;
|
|
1932
|
+
if (!databaseCommitsSuspended(rootStore)) return;
|
|
1933
|
+
const retryable = !this.root && !this.snapshotFree;
|
|
1934
|
+
if (this.transaction === transaction) this.detachOwnedTransaction();
|
|
1935
|
+
abortNativeTransaction(transaction, 'aborting a transaction while its database is closing');
|
|
1936
|
+
try {
|
|
1937
|
+
this.abort();
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
harperLogger.debug?.('cleaning up a transaction while its database is closing', error);
|
|
1940
|
+
}
|
|
1941
|
+
throw new DatabaseClosingError(rootStore?.databaseName ?? rootStore?.path ?? 'unknown', retryable);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1798
1944
|
abort(): void {
|
|
1799
1945
|
while (this.readTxnsUsed > 0) this.doneReadTxn(); // release the read snapshot when we abort, we assume we don't need it
|
|
1800
1946
|
// Defensively release any native handle whose reference bookkeeping was already consumed.
|
|
@@ -1967,6 +2113,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
1967
2113
|
}
|
|
1968
2114
|
directCommitSync(): void {
|
|
1969
2115
|
const transaction = this.transaction;
|
|
2116
|
+
if (transaction) this.assertDatabaseCommitAllowed(transaction);
|
|
1970
2117
|
try {
|
|
1971
2118
|
transaction?.commitSync();
|
|
1972
2119
|
} catch (error) {
|