@bsv/overlay 2.3.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/LICENSES/Open-BSV-License-4-standard-pre-2026.txt +28 -0
  2. package/README.md +171 -1
  3. package/THIRD_PARTY_NOTICES.md +49 -0
  4. package/dist/cjs/mod.d.ts +4 -1
  5. package/dist/cjs/mod.d.ts.map +1 -1
  6. package/dist/cjs/mod.js +8 -1
  7. package/dist/cjs/mod.js.map +1 -1
  8. package/dist/cjs/package.json +13 -5
  9. package/dist/cjs/src/BASM.d.ts +6 -0
  10. package/dist/cjs/src/BASM.d.ts.map +1 -1
  11. package/dist/cjs/src/BASM.js +21 -8
  12. package/dist/cjs/src/BASM.js.map +1 -1
  13. package/dist/cjs/src/BASMRemote.d.ts +8 -1
  14. package/dist/cjs/src/BASMRemote.d.ts.map +1 -1
  15. package/dist/cjs/src/BASMRemote.js +179 -13
  16. package/dist/cjs/src/BASMRemote.js.map +1 -1
  17. package/dist/cjs/src/BASMValidation.d.ts +27 -0
  18. package/dist/cjs/src/BASMValidation.d.ts.map +1 -0
  19. package/dist/cjs/src/BASMValidation.js +108 -0
  20. package/dist/cjs/src/BASMValidation.js.map +1 -0
  21. package/dist/cjs/src/DiscoveryAdvertisementValidation.d.ts +11 -0
  22. package/dist/cjs/src/DiscoveryAdvertisementValidation.d.ts.map +1 -0
  23. package/dist/cjs/src/DiscoveryAdvertisementValidation.js +60 -0
  24. package/dist/cjs/src/DiscoveryAdvertisementValidation.js.map +1 -0
  25. package/dist/cjs/src/Engine.d.ts +38 -13
  26. package/dist/cjs/src/Engine.d.ts.map +1 -1
  27. package/dist/cjs/src/Engine.js +1735 -336
  28. package/dist/cjs/src/Engine.js.map +1 -1
  29. package/dist/cjs/src/EngineAdmission.d.ts +70 -0
  30. package/dist/cjs/src/EngineAdmission.d.ts.map +1 -0
  31. package/dist/cjs/src/EngineAdmission.js +297 -0
  32. package/dist/cjs/src/EngineAdmission.js.map +1 -0
  33. package/dist/cjs/src/GASP/OverlayGASPRemote.d.ts +4 -3
  34. package/dist/cjs/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
  35. package/dist/cjs/src/GASP/OverlayGASPRemote.js +27 -43
  36. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
  37. package/dist/cjs/src/GASP/OverlayGASPStorage.d.ts +22 -16
  38. package/dist/cjs/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  39. package/dist/cjs/src/GASP/OverlayGASPStorage.js +206 -57
  40. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
  41. package/dist/cjs/src/RemoteSecurity.d.ts +50 -0
  42. package/dist/cjs/src/RemoteSecurity.d.ts.map +1 -0
  43. package/dist/cjs/src/RemoteSecurity.js +301 -0
  44. package/dist/cjs/src/RemoteSecurity.js.map +1 -0
  45. package/dist/cjs/src/TopicManager.d.ts +13 -3
  46. package/dist/cjs/src/TopicManager.d.ts.map +1 -1
  47. package/dist/cjs/src/storage/AdmissionStorage.d.ts +185 -0
  48. package/dist/cjs/src/storage/AdmissionStorage.d.ts.map +1 -0
  49. package/dist/cjs/src/storage/AdmissionStorage.js +93 -0
  50. package/dist/cjs/src/storage/AdmissionStorage.js.map +1 -0
  51. package/dist/cjs/src/storage/RecoveryContract.d.ts +50 -0
  52. package/dist/cjs/src/storage/RecoveryContract.d.ts.map +1 -0
  53. package/dist/cjs/src/storage/RecoveryContract.js +35 -0
  54. package/dist/cjs/src/storage/RecoveryContract.js.map +1 -0
  55. package/dist/cjs/src/storage/Storage.d.ts +32 -19
  56. package/dist/cjs/src/storage/Storage.d.ts.map +1 -1
  57. package/dist/cjs/src/storage/Storage.js +5 -0
  58. package/dist/cjs/src/storage/Storage.js.map +1 -1
  59. package/dist/cjs/src/storage/knex/KnexStorage.d.ts +8 -8
  60. package/dist/cjs/src/storage/knex/KnexStorage.d.ts.map +1 -1
  61. package/dist/cjs/src/storage/knex/KnexStorage.js +98 -61
  62. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  63. package/dist/cjs/src/storage/knex/all-migrations.d.ts.map +1 -1
  64. package/dist/cjs/src/storage/knex/all-migrations.js +5 -1
  65. package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
  66. package/dist/cjs/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.d.ts +4 -0
  67. package/dist/cjs/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.d.ts.map +1 -0
  68. package/dist/cjs/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.js +36 -0
  69. package/dist/cjs/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.js.map +1 -0
  70. package/dist/cjs/src/storage/knex/migrations/2026-09-20-001-spent-by.d.ts +5 -0
  71. package/dist/cjs/src/storage/knex/migrations/2026-09-20-001-spent-by.d.ts.map +1 -0
  72. package/dist/cjs/src/storage/knex/migrations/2026-09-20-001-spent-by.js +16 -0
  73. package/dist/cjs/src/storage/knex/migrations/2026-09-20-001-spent-by.js.map +1 -0
  74. package/dist/cjs/src/storage/mongo/MongoAdmissionPlan.d.ts +18 -0
  75. package/dist/cjs/src/storage/mongo/MongoAdmissionPlan.d.ts.map +1 -0
  76. package/dist/cjs/src/storage/mongo/MongoAdmissionPlan.js +229 -0
  77. package/dist/cjs/src/storage/mongo/MongoAdmissionPlan.js.map +1 -0
  78. package/dist/cjs/src/storage/mongo/MongoAdmissionStorage.d.ts +112 -0
  79. package/dist/cjs/src/storage/mongo/MongoAdmissionStorage.d.ts.map +1 -0
  80. package/dist/cjs/src/storage/mongo/MongoAdmissionStorage.js +675 -0
  81. package/dist/cjs/src/storage/mongo/MongoAdmissionStorage.js.map +1 -0
  82. package/dist/cjs/src/storage/mongo/MongoOverlayStorage.d.ts +71 -0
  83. package/dist/cjs/src/storage/mongo/MongoOverlayStorage.d.ts.map +1 -0
  84. package/dist/cjs/src/storage/mongo/MongoOverlayStorage.js +453 -0
  85. package/dist/cjs/src/storage/mongo/MongoOverlayStorage.js.map +1 -0
  86. package/dist/cjs/src/storage/mongo/MongoPayloadStore.d.ts +134 -0
  87. package/dist/cjs/src/storage/mongo/MongoPayloadStore.d.ts.map +1 -0
  88. package/dist/cjs/src/storage/mongo/MongoPayloadStore.js +756 -0
  89. package/dist/cjs/src/storage/mongo/MongoPayloadStore.js.map +1 -0
  90. package/dist/cjs/src/storage/mongo/MongoReadGuards.d.ts +44 -0
  91. package/dist/cjs/src/storage/mongo/MongoReadGuards.d.ts.map +1 -0
  92. package/dist/cjs/src/storage/mongo/MongoReadGuards.js +135 -0
  93. package/dist/cjs/src/storage/mongo/MongoReadGuards.js.map +1 -0
  94. package/dist/cjs/src/storage/mongo/MongoSchema.d.ts +67 -0
  95. package/dist/cjs/src/storage/mongo/MongoSchema.d.ts.map +1 -0
  96. package/dist/cjs/src/storage/mongo/MongoSchema.js +743 -0
  97. package/dist/cjs/src/storage/mongo/MongoSchema.js.map +1 -0
  98. package/dist/cjs/src/storage/mongo/MongoTransactionRunner.d.ts +69 -0
  99. package/dist/cjs/src/storage/mongo/MongoTransactionRunner.d.ts.map +1 -0
  100. package/dist/cjs/src/storage/mongo/MongoTransactionRunner.js +368 -0
  101. package/dist/cjs/src/storage/mongo/MongoTransactionRunner.js.map +1 -0
  102. package/dist/cjs/src/storage/mongo.d.ts +9 -0
  103. package/dist/cjs/src/storage/mongo.d.ts.map +1 -0
  104. package/dist/cjs/src/storage/mongo.js +25 -0
  105. package/dist/cjs/src/storage/mongo.js.map +1 -0
  106. package/dist/esm/mod.js +3 -0
  107. package/dist/esm/mod.js.map +1 -1
  108. package/dist/esm/src/BASM.js +21 -8
  109. package/dist/esm/src/BASM.js.map +1 -1
  110. package/dist/esm/src/BASMRemote.js +176 -12
  111. package/dist/esm/src/BASMRemote.js.map +1 -1
  112. package/dist/esm/src/BASMValidation.js +95 -0
  113. package/dist/esm/src/BASMValidation.js.map +1 -0
  114. package/dist/esm/src/DiscoveryAdvertisementValidation.js +57 -0
  115. package/dist/esm/src/DiscoveryAdvertisementValidation.js.map +1 -0
  116. package/dist/esm/src/Engine.js +1720 -326
  117. package/dist/esm/src/Engine.js.map +1 -1
  118. package/dist/esm/src/EngineAdmission.js +280 -0
  119. package/dist/esm/src/EngineAdmission.js.map +1 -0
  120. package/dist/esm/src/GASP/OverlayGASPRemote.js +27 -43
  121. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  122. package/dist/esm/src/GASP/OverlayGASPStorage.js +200 -56
  123. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  124. package/dist/esm/src/RemoteSecurity.js +279 -0
  125. package/dist/esm/src/RemoteSecurity.js.map +1 -0
  126. package/dist/esm/src/storage/AdmissionStorage.js +85 -0
  127. package/dist/esm/src/storage/AdmissionStorage.js.map +1 -0
  128. package/dist/esm/src/storage/RecoveryContract.js +31 -0
  129. package/dist/esm/src/storage/RecoveryContract.js.map +1 -0
  130. package/dist/esm/src/storage/Storage.js +4 -1
  131. package/dist/esm/src/storage/Storage.js.map +1 -1
  132. package/dist/esm/src/storage/knex/KnexStorage.js +98 -61
  133. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  134. package/dist/esm/src/storage/knex/all-migrations.js +5 -1
  135. package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
  136. package/dist/esm/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.js +32 -0
  137. package/dist/esm/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.js.map +1 -0
  138. package/dist/esm/src/storage/knex/migrations/2026-09-20-001-spent-by.js +12 -0
  139. package/dist/esm/src/storage/knex/migrations/2026-09-20-001-spent-by.js.map +1 -0
  140. package/dist/esm/src/storage/mongo/MongoAdmissionPlan.js +217 -0
  141. package/dist/esm/src/storage/mongo/MongoAdmissionPlan.js.map +1 -0
  142. package/dist/esm/src/storage/mongo/MongoAdmissionStorage.js +673 -0
  143. package/dist/esm/src/storage/mongo/MongoAdmissionStorage.js.map +1 -0
  144. package/dist/esm/src/storage/mongo/MongoOverlayStorage.js +451 -0
  145. package/dist/esm/src/storage/mongo/MongoOverlayStorage.js.map +1 -0
  146. package/dist/esm/src/storage/mongo/MongoPayloadStore.js +747 -0
  147. package/dist/esm/src/storage/mongo/MongoPayloadStore.js.map +1 -0
  148. package/dist/esm/src/storage/mongo/MongoReadGuards.js +129 -0
  149. package/dist/esm/src/storage/mongo/MongoReadGuards.js.map +1 -0
  150. package/dist/esm/src/storage/mongo/MongoSchema.js +730 -0
  151. package/dist/esm/src/storage/mongo/MongoSchema.js.map +1 -0
  152. package/dist/esm/src/storage/mongo/MongoTransactionRunner.js +371 -0
  153. package/dist/esm/src/storage/mongo/MongoTransactionRunner.js.map +1 -0
  154. package/dist/esm/src/storage/mongo.js +9 -0
  155. package/dist/esm/src/storage/mongo.js.map +1 -0
  156. package/dist/types/mod.d.ts +4 -1
  157. package/dist/types/mod.d.ts.map +1 -1
  158. package/dist/types/src/BASM.d.ts +6 -0
  159. package/dist/types/src/BASM.d.ts.map +1 -1
  160. package/dist/types/src/BASMRemote.d.ts +8 -1
  161. package/dist/types/src/BASMRemote.d.ts.map +1 -1
  162. package/dist/types/src/BASMValidation.d.ts +27 -0
  163. package/dist/types/src/BASMValidation.d.ts.map +1 -0
  164. package/dist/types/src/DiscoveryAdvertisementValidation.d.ts +11 -0
  165. package/dist/types/src/DiscoveryAdvertisementValidation.d.ts.map +1 -0
  166. package/dist/types/src/Engine.d.ts +38 -13
  167. package/dist/types/src/Engine.d.ts.map +1 -1
  168. package/dist/types/src/EngineAdmission.d.ts +70 -0
  169. package/dist/types/src/EngineAdmission.d.ts.map +1 -0
  170. package/dist/types/src/GASP/OverlayGASPRemote.d.ts +4 -3
  171. package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
  172. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +22 -16
  173. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  174. package/dist/types/src/RemoteSecurity.d.ts +50 -0
  175. package/dist/types/src/RemoteSecurity.d.ts.map +1 -0
  176. package/dist/types/src/TopicManager.d.ts +13 -3
  177. package/dist/types/src/TopicManager.d.ts.map +1 -1
  178. package/dist/types/src/storage/AdmissionStorage.d.ts +185 -0
  179. package/dist/types/src/storage/AdmissionStorage.d.ts.map +1 -0
  180. package/dist/types/src/storage/RecoveryContract.d.ts +50 -0
  181. package/dist/types/src/storage/RecoveryContract.d.ts.map +1 -0
  182. package/dist/types/src/storage/Storage.d.ts +32 -19
  183. package/dist/types/src/storage/Storage.d.ts.map +1 -1
  184. package/dist/types/src/storage/knex/KnexStorage.d.ts +8 -8
  185. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  186. package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
  187. package/dist/types/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.d.ts +4 -0
  188. package/dist/types/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.d.ts.map +1 -0
  189. package/dist/types/src/storage/knex/migrations/2026-09-20-001-spent-by.d.ts +5 -0
  190. package/dist/types/src/storage/knex/migrations/2026-09-20-001-spent-by.d.ts.map +1 -0
  191. package/dist/types/src/storage/mongo/MongoAdmissionPlan.d.ts +18 -0
  192. package/dist/types/src/storage/mongo/MongoAdmissionPlan.d.ts.map +1 -0
  193. package/dist/types/src/storage/mongo/MongoAdmissionStorage.d.ts +112 -0
  194. package/dist/types/src/storage/mongo/MongoAdmissionStorage.d.ts.map +1 -0
  195. package/dist/types/src/storage/mongo/MongoOverlayStorage.d.ts +71 -0
  196. package/dist/types/src/storage/mongo/MongoOverlayStorage.d.ts.map +1 -0
  197. package/dist/types/src/storage/mongo/MongoPayloadStore.d.ts +134 -0
  198. package/dist/types/src/storage/mongo/MongoPayloadStore.d.ts.map +1 -0
  199. package/dist/types/src/storage/mongo/MongoReadGuards.d.ts +44 -0
  200. package/dist/types/src/storage/mongo/MongoReadGuards.d.ts.map +1 -0
  201. package/dist/types/src/storage/mongo/MongoSchema.d.ts +67 -0
  202. package/dist/types/src/storage/mongo/MongoSchema.d.ts.map +1 -0
  203. package/dist/types/src/storage/mongo/MongoTransactionRunner.d.ts +69 -0
  204. package/dist/types/src/storage/mongo/MongoTransactionRunner.d.ts.map +1 -0
  205. package/dist/types/src/storage/mongo.d.ts +9 -0
  206. package/dist/types/src/storage/mongo.d.ts.map +1 -0
  207. package/package.json +14 -6
@@ -0,0 +1,673 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { admissionSemanticDigest, asStorageUint64, isReplaySafeProjection } from '../AdmissionStorage.js';
4
+ import { AdmissionRejectedError, admissionPlanPayloads, admissionReceiptFor, lookupOutboxIntents, propagationOutboxIntents, rejectAdmission, sameScope, validateAdmissionPlan } from './MongoAdmissionPlan.js';
5
+ import { MongoPayloadStore } from './MongoPayloadStore.js';
6
+ import { MongoReadGuardConflictError, MongoReadGuards } from './MongoReadGuards.js';
7
+ import { MongoCollectionNames, encodeMongoOutputIndex, encodeMongoUint64, decodeMongoUint64, mongoChainKey, mongoNodeKey, mongoRecordKey } from './MongoSchema.js';
8
+ import { MongoTransactionRunner } from './MongoTransactionRunner.js';
9
+ function isMongoWriteConflict(error) {
10
+ if (typeof error !== 'object' || error === null)
11
+ return false;
12
+ const code = 'code' in error ? error.code : undefined;
13
+ if (code === 112 || code === '112')
14
+ return true;
15
+ if ('codeName' in error && error.codeName === 'WriteConflict')
16
+ return true;
17
+ const text = [
18
+ error instanceof Error ? error.message : '',
19
+ 'errmsg' in error ? String(error.errmsg) : ''
20
+ ].join(' ');
21
+ return text.includes('Write conflict');
22
+ }
23
+ const majority = { w: 'majority', j: true };
24
+ const schemaVersion = 1;
25
+ const duplicateKey = (error) => typeof error === 'object' && error !== null && 'code' in error && error.code === 11000;
26
+ const isPayloadKind = (value) => value === 'raw-transaction' ||
27
+ value === 'merkle-path' ||
28
+ value === 'beef-manifest' ||
29
+ value === 'locking-script' ||
30
+ value === 'outbox-data';
31
+ /**
32
+ * Opt-in Mongo AdmissionStorage. Engine submit uses this only when
33
+ * getAdmissionStorage(storage) observes overlay-admission-v1.
34
+ */
35
+ export class MongoAdmissionStorage {
36
+ db;
37
+ protocol = 'overlay-admission-v1';
38
+ scope;
39
+ runner;
40
+ payloads;
41
+ guards;
42
+ enlisted;
43
+ projector;
44
+ peers = new Map();
45
+ constructor(db, scope, options = {}) {
46
+ this.db = db;
47
+ this.scope = { ...scope };
48
+ this.runner = options.runner ?? new MongoTransactionRunner(db, this.scope);
49
+ this.payloads = options.payloads ?? new MongoPayloadStore(db, this.scope);
50
+ this.guards = options.readGuards ?? new MongoReadGuards(db);
51
+ this.enlisted = [...(options.enlistedIndexes ?? [])];
52
+ this.projector = options.projector;
53
+ if (this.enlisted.some(index => index.protocol !== 'overlay-mongo-index-v1' || index.target.length === 0)) {
54
+ throw new Error('Invalid enlisted Mongo lookup index');
55
+ }
56
+ if (new Set(this.enlisted.map(index => index.target)).size !== this.enlisted.length) {
57
+ throw new Error('Duplicate enlisted Mongo lookup index target');
58
+ }
59
+ }
60
+ enlistedTargets() {
61
+ return this.enlisted.map(index => index.target);
62
+ }
63
+ async commitAdmission(plan) {
64
+ if (!sameScope(plan.key.scope, this.scope) || !sameScope(plan.identity.scope, this.scope)) {
65
+ return await this.peerFor(plan.key.scope).commitAdmission(plan);
66
+ }
67
+ let semanticDigest;
68
+ try {
69
+ semanticDigest = admissionSemanticDigest(plan.identity);
70
+ }
71
+ catch {
72
+ return { state: 'rejected', code: 'digest-mismatch' };
73
+ }
74
+ if (plan.key.semanticDigest !== semanticDigest ||
75
+ !sameScope(plan.key.scope, plan.identity.scope)) {
76
+ return { state: 'rejected', code: 'digest-mismatch' };
77
+ }
78
+ if (this.projector !== undefined && !isReplaySafeProjection(this.projector)) {
79
+ return { state: 'rejected', code: 'unsupported-projection' };
80
+ }
81
+ const existing = await this.runner.reconcile(plan.key);
82
+ if (existing.state === 'committed' || existing.state === 'rejected')
83
+ return existing;
84
+ if (existing.state === 'pending' && existing.attemptId !== 'unlocated') {
85
+ const waited = await this.waitForPending(plan.key, existing.attemptId);
86
+ if (waited.state === 'committed' || waited.state === 'rejected')
87
+ return waited;
88
+ }
89
+ const rejected = validateAdmissionPlan(plan);
90
+ if (rejected !== undefined)
91
+ return { state: 'rejected', code: rejected };
92
+ let receipt;
93
+ try {
94
+ receipt = admissionReceiptFor(plan, this.enlistedTargets());
95
+ }
96
+ catch (error) {
97
+ return this.asResult(error);
98
+ }
99
+ await this.prepareReadGuards(plan);
100
+ await this.publishHistoryUpdatePayloads(plan);
101
+ return await this.runAdmissionWithWriteConflictRetry(plan, receipt);
102
+ }
103
+ async runAdmissionWithWriteConflictRetry(plan, receipt) {
104
+ for (let attempt = 0; attempt < 8; attempt += 1) {
105
+ try {
106
+ const result = await this.runner.run({ key: plan.key, identity: plan.identity, receipt }, async (context) => {
107
+ await this.applyPlan(context, plan);
108
+ });
109
+ if (result.state !== 'pending')
110
+ return result;
111
+ return await this.waitForPending(plan.key, result.attemptId);
112
+ }
113
+ catch (error) {
114
+ if (isMongoWriteConflict(error) && attempt < 7)
115
+ continue;
116
+ return this.asResult(error);
117
+ }
118
+ }
119
+ return { state: 'rejected', code: 'spend-conflict' };
120
+ }
121
+ async waitForPending(key, attemptId) {
122
+ let current = { state: 'pending', attemptId };
123
+ for (let index = 0; index < 50 && current.state === 'pending'; index += 1) {
124
+ await delay(20);
125
+ const reconciled = await this.runner.reconcile(key, current.state === 'pending' ? current.attemptId : attemptId);
126
+ if (reconciled.state === 'committed' || reconciled.state === 'rejected')
127
+ return reconciled;
128
+ if (reconciled.state === 'aborted') {
129
+ return { state: 'pending', attemptId };
130
+ }
131
+ current = reconciled;
132
+ }
133
+ return current;
134
+ }
135
+ async reconcileAdmission(key, attemptId, options = {}) {
136
+ if (!sameScope(key.scope, this.scope))
137
+ return await this.peerFor(key.scope).reconcileAdmission(key, attemptId, options);
138
+ return await this.runner.reconcile(key, attemptId, options);
139
+ }
140
+ async close() {
141
+ await Promise.all([
142
+ this.runner.close(),
143
+ ...[...this.peers.values()].map(async (peer) => await peer.close())
144
+ ]);
145
+ }
146
+ peerFor(scope) {
147
+ const id = mongoNodeKey(scope);
148
+ const existing = this.peers.get(id);
149
+ if (existing !== undefined)
150
+ return existing;
151
+ const peer = new MongoAdmissionStorage(this.db, scope, {
152
+ enlistedIndexes: this.enlisted,
153
+ projector: this.projector
154
+ });
155
+ this.peers.set(id, peer);
156
+ return peer;
157
+ }
158
+ async claimOutbox(kind, leaseMS = 15_000) {
159
+ if (!Number.isSafeInteger(leaseMS) || leaseMS < 1 || leaseMS > 60_000) {
160
+ throw new Error('Invalid Mongo outbox lease');
161
+ }
162
+ const claimed = await this.outbox(kind).findOneAndUpdate({
163
+ network: this.scope.network,
164
+ genesisHash: this.scope.genesisHash,
165
+ nodeId: this.scope.nodeId,
166
+ $or: [{ state: 'pending' }, { state: 'leased', $expr: { $lte: ['$leaseUntil', '$$NOW'] } }]
167
+ }, [
168
+ {
169
+ $set: {
170
+ state: 'leased',
171
+ leaseUntil: { $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: leaseMS } },
172
+ nextAttemptAt: {
173
+ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: leaseMS }
174
+ },
175
+ updatedAt: '$$NOW'
176
+ }
177
+ }
178
+ ], { returnDocument: 'after', writeConcern: majority });
179
+ if (claimed === null)
180
+ return null;
181
+ return {
182
+ eventId: claimed.eventId,
183
+ kind,
184
+ target: claimed.target,
185
+ payloads: await this.outboxPayloads(kind, claimed.eventId)
186
+ };
187
+ }
188
+ async acknowledgeOutbox(kind, eventId) {
189
+ const result = await this.outbox(kind).updateOne({
190
+ _id: this.outboxId(kind, eventId),
191
+ eventId,
192
+ state: { $in: ['leased', 'delivered'] }
193
+ }, { $set: { state: 'delivered', updatedAt: new Date() } }, { writeConcern: majority });
194
+ if (result.matchedCount !== 1)
195
+ throw new Error('Mongo outbox event is not leased');
196
+ }
197
+ asResult(error) {
198
+ if (error instanceof AdmissionRejectedError)
199
+ return { state: 'rejected', code: error.code };
200
+ if (error instanceof MongoReadGuardConflictError)
201
+ return { state: 'rejected', code: 'read-conflict' };
202
+ if (error instanceof Error && error.message === 'Mongo payload is not ready for reference') {
203
+ return { state: 'rejected', code: 'payload-not-ready' };
204
+ }
205
+ throw error;
206
+ }
207
+ async prepareReadGuards(plan) {
208
+ for (const decision of plan.decisions) {
209
+ for (const read of decision.reads)
210
+ await this.guards.initialize(this.scope, read.key);
211
+ }
212
+ }
213
+ async publishHistoryUpdatePayloads(plan) {
214
+ for (const decision of plan.decisions) {
215
+ if (decision.historyUpdate === undefined)
216
+ continue;
217
+ const bytes = Buffer.from(JSON.stringify(this.historyUpdateRecord(decision)), 'utf8');
218
+ const digest = createHash('sha256').update(bytes).digest('hex');
219
+ await this.payloads.publish({
220
+ kind: 'outbox-data',
221
+ digest,
222
+ byteLength: String(bytes.byteLength),
223
+ bytes: (async function* () {
224
+ yield bytes;
225
+ })()
226
+ });
227
+ }
228
+ }
229
+ historyUpdateRecord(decision) {
230
+ if (decision.historyUpdate === undefined)
231
+ return { affectedFromHeight: '0' };
232
+ return {
233
+ affectedFromHeight: decision.historyUpdate.affectedFromHeight,
234
+ ...(decision.historyUpdate.handoff === undefined
235
+ ? {}
236
+ : { checkpoint: decision.historyUpdate.handoff.checkpoint })
237
+ };
238
+ }
239
+ async applyPlan(context, plan) {
240
+ await this.assertReadyPayloads(context, plan);
241
+ for (const decision of plan.decisions) {
242
+ await this.checkReads(context, decision);
243
+ await this.checkHistory(context, decision);
244
+ await this.assertAppliedAvailable(context, plan, decision);
245
+ }
246
+ for (const intent of plan.outbox)
247
+ await this.assertOutboxAvailable(context, intent);
248
+ for (const decision of plan.decisions) {
249
+ for (const spend of decision.spends)
250
+ await this.applySpend(context, decision.topic, spend);
251
+ for (const eviction of decision.evictions)
252
+ await this.applyEviction(context, decision.topic, eviction);
253
+ for (const output of decision.outputs)
254
+ await this.insertOutput(context, decision, output);
255
+ for (const edge of decision.edges)
256
+ await this.insertEdge(context, decision.topic, edge);
257
+ await this.insertApplied(context, plan, decision);
258
+ await this.applyHistoryUpdate(context, decision, plan.identity.topics.find(item => item.topic === decision.topic)?.policyId ?? 'default');
259
+ }
260
+ await this.upsertTransaction(context, plan);
261
+ for (const ref of admissionPlanPayloads(plan)) {
262
+ await this.pin(context, ref, 'transaction', plan.identity.txid, `${ref.kind}:${ref.digest}`);
263
+ }
264
+ for (const intent of lookupOutboxIntents(plan))
265
+ await this.insertOutbox(context, 'lookup', intent);
266
+ for (const intent of propagationOutboxIntents(plan)) {
267
+ await this.insertOutbox(context, 'propagation', intent);
268
+ }
269
+ for (const index of this.enlisted)
270
+ await index.apply(context, plan);
271
+ }
272
+ async assertReadyPayloads(context, plan) {
273
+ const seen = new Set();
274
+ for (const ref of admissionPlanPayloads(plan)) {
275
+ const id = this.payloadId(ref);
276
+ if (seen.has(id))
277
+ continue;
278
+ seen.add(id);
279
+ const found = await this.db
280
+ .collection(MongoCollectionNames.payloads)
281
+ .findOne({ _id: id, state: 'ready', digest: ref.digest, kind: ref.kind }, context.options());
282
+ if (found === null || decodeMongoUint64(String(found.byteLength)) !== ref.byteLength) {
283
+ rejectAdmission('payload-not-ready');
284
+ }
285
+ }
286
+ }
287
+ async checkReads(context, decision) {
288
+ for (const read of decision.reads) {
289
+ const options = context.options();
290
+ await this.guards.check(options.session, { scope: this.scope, key: read.key, expectedVersion: read.expectedVersion }, { timeoutMS: options.timeoutMS, signal: context.signal });
291
+ }
292
+ }
293
+ async checkHistory(context, decision) {
294
+ const document = await this.generations().findOne({ _id: this.generationId(decision.topic) }, context.options());
295
+ const epoch = document === null ? '0' : decodeMongoUint64(document.chainEpoch);
296
+ const generation = document === null ? '0' : decodeMongoUint64(document.topicHistoryGeneration);
297
+ if (epoch !== decision.expectedHistory.chainEpoch ||
298
+ generation !== decision.expectedHistory.topicHistoryGeneration) {
299
+ rejectAdmission('read-conflict');
300
+ }
301
+ }
302
+ async assertAppliedAvailable(context, plan, decision) {
303
+ const existing = await this.applied().findOne({ _id: this.appliedId(decision.topic, decision.applied.txid) }, context.options());
304
+ if (existing !== null && existing.admissionId !== plan.key.operationId) {
305
+ rejectAdmission('invalid-plan');
306
+ }
307
+ }
308
+ async assertOutboxAvailable(context, intent) {
309
+ const existing = await this.outbox(intent.kind).findOne({ _id: this.outboxId(intent.kind, intent.eventId) }, context.options());
310
+ if (existing !== null)
311
+ rejectAdmission('invalid-plan');
312
+ }
313
+ async applySpend(context, topic, spend) {
314
+ const id = this.outputId(topic, spend.outpoint);
315
+ const result = await this.outputs().findOneAndUpdate({
316
+ _id: id,
317
+ version: spend.expectedVersion,
318
+ state: 'unspent'
319
+ }, {
320
+ $set: {
321
+ state: 'spent',
322
+ spender: spend.spender,
323
+ updatedAt: new Date()
324
+ }
325
+ }, { ...context.options(), returnDocument: 'after' });
326
+ if (result === null) {
327
+ const current = await this.outputs().findOne({ _id: id }, context.options());
328
+ if (current?.spender === spend.spender && current.state === 'spent')
329
+ return;
330
+ rejectAdmission('spend-conflict');
331
+ }
332
+ }
333
+ async applyEviction(context, topic, eviction) {
334
+ const id = this.outputId(topic, eviction);
335
+ const result = await this.outputs().updateOne({ _id: id }, { $set: { state: 'evicted', updatedAt: new Date() } }, context.options());
336
+ if (result.matchedCount !== 1)
337
+ rejectAdmission('invalid-plan');
338
+ }
339
+ async insertOutput(context, decision, output) {
340
+ const now = new Date();
341
+ const payloadId = this.payloadId(output.script.payload);
342
+ try {
343
+ await this.outputs().insertOne({
344
+ _id: this.outputId(decision.topic, output),
345
+ schemaVersion,
346
+ network: this.scope.network,
347
+ genesisHash: this.scope.genesisHash,
348
+ nodeId: this.scope.nodeId,
349
+ topic: decision.topic,
350
+ txid: output.txid,
351
+ outputIndex: encodeMongoOutputIndex(output.outputIndex),
352
+ satoshis: encodeMongoUint64(output.satoshis),
353
+ score: encodeMongoUint64(output.score),
354
+ scriptPayloadId: payloadId,
355
+ scriptOffset: encodeMongoUint64(output.script.offset),
356
+ scriptByteLength: encodeMongoUint64(output.script.byteLength),
357
+ state: 'unspent',
358
+ version: '1',
359
+ createdAt: now,
360
+ updatedAt: now
361
+ }, context.options());
362
+ }
363
+ catch (error) {
364
+ if (duplicateKey(error))
365
+ rejectAdmission('invalid-plan');
366
+ throw error;
367
+ }
368
+ await this.pin(context, output.script.payload, 'output', this.outputId(decision.topic, output), 'script');
369
+ }
370
+ async insertEdge(context, topic, edge) {
371
+ const now = new Date();
372
+ const id = mongoRecordKey(mongoNodeKey(this.scope), 'edge', topic, edge.source.txid, encodeMongoOutputIndex(edge.source.outputIndex), edge.consumer.txid, encodeMongoOutputIndex(edge.consumer.outputIndex));
373
+ await this.db.collection(MongoCollectionNames.consumptionEdges).updateOne({ _id: id }, {
374
+ $setOnInsert: {
375
+ _id: id,
376
+ schemaVersion,
377
+ network: this.scope.network,
378
+ genesisHash: this.scope.genesisHash,
379
+ nodeId: this.scope.nodeId,
380
+ topic,
381
+ sourceTxid: edge.source.txid,
382
+ sourceOutputIndex: encodeMongoOutputIndex(edge.source.outputIndex),
383
+ consumerTxid: edge.consumer.txid,
384
+ consumerOutputIndex: encodeMongoOutputIndex(edge.consumer.outputIndex),
385
+ createdAt: now
386
+ },
387
+ $set: { updatedAt: now }
388
+ }, { upsert: true, ...context.options() });
389
+ }
390
+ async insertApplied(context, plan, decision) {
391
+ const now = new Date();
392
+ const proven = decision.applied.block !== undefined;
393
+ try {
394
+ await this.applied().insertOne({
395
+ _id: this.appliedId(decision.topic, decision.applied.txid),
396
+ schemaVersion,
397
+ network: this.scope.network,
398
+ genesisHash: this.scope.genesisHash,
399
+ nodeId: this.scope.nodeId,
400
+ topic: decision.topic,
401
+ txid: decision.applied.txid,
402
+ state: proven ? 'active' : 'unproven',
403
+ admissionId: plan.key.operationId,
404
+ ...(decision.applied.firstSeenHeight === undefined
405
+ ? {}
406
+ : { firstSeenHeight: encodeMongoUint64(decision.applied.firstSeenHeight) }),
407
+ ...(decision.applied.proof === undefined
408
+ ? {}
409
+ : { proofPayloadId: this.payloadId(decision.applied.proof) }),
410
+ createdAt: now,
411
+ updatedAt: now
412
+ }, context.options());
413
+ }
414
+ catch (error) {
415
+ if (duplicateKey(error))
416
+ rejectAdmission('invalid-plan');
417
+ throw error;
418
+ }
419
+ if (decision.applied.proof !== undefined) {
420
+ await this.pin(context, decision.applied.proof, 'applied-history', this.appliedId(decision.topic, decision.applied.txid), 'proof');
421
+ }
422
+ }
423
+ async applyHistoryUpdate(context, decision, policyId) {
424
+ if (decision.historyUpdate === undefined)
425
+ return;
426
+ const now = new Date();
427
+ const next = decision.historyUpdate.nextTopicHistoryGeneration;
428
+ const updated = await this.generations().findOneAndUpdate({
429
+ _id: this.generationId(decision.topic),
430
+ chainEpoch: encodeMongoUint64(decision.expectedHistory.chainEpoch),
431
+ topicHistoryGeneration: encodeMongoUint64(decision.expectedHistory.topicHistoryGeneration)
432
+ }, {
433
+ $set: {
434
+ topicHistoryGeneration: encodeMongoUint64(next),
435
+ policyId,
436
+ updatedAt: now
437
+ }
438
+ }, { ...context.options(), returnDocument: 'after' });
439
+ if (updated === null) {
440
+ if (decision.expectedHistory.chainEpoch !== '0' ||
441
+ decision.expectedHistory.topicHistoryGeneration !== '0') {
442
+ rejectAdmission('read-conflict');
443
+ }
444
+ try {
445
+ await this.generations().insertOne({
446
+ _id: this.generationId(decision.topic),
447
+ schemaVersion,
448
+ network: this.scope.network,
449
+ genesisHash: this.scope.genesisHash,
450
+ nodeId: this.scope.nodeId,
451
+ topic: decision.topic,
452
+ chainEpoch: encodeMongoUint64(decision.expectedHistory.chainEpoch),
453
+ topicHistoryGeneration: encodeMongoUint64(next),
454
+ policyId,
455
+ createdAt: now,
456
+ updatedAt: now
457
+ }, context.options());
458
+ }
459
+ catch (error) {
460
+ if (duplicateKey(error))
461
+ rejectAdmission('read-conflict');
462
+ throw error;
463
+ }
464
+ }
465
+ if (decision.historyUpdate.handoff !== undefined) {
466
+ await this.applyHandoff(context, decision, decision.historyUpdate.handoff.expected, next);
467
+ }
468
+ const bytes = Buffer.from(JSON.stringify(this.historyUpdateRecord(decision)), 'utf8');
469
+ const digest = createHash('sha256').update(bytes).digest('hex');
470
+ const ref = {
471
+ kind: 'outbox-data',
472
+ digest,
473
+ byteLength: asStorageUint64(String(bytes.byteLength))
474
+ };
475
+ const payload = await this.db
476
+ .collection(MongoCollectionNames.payloads)
477
+ .findOne({ _id: this.payloadId(ref), state: 'ready' }, context.options());
478
+ if (payload === null)
479
+ rejectAdmission('payload-not-ready');
480
+ await this.replaceHistoryPin(context, decision.topic, decision.expectedHistory.topicHistoryGeneration, next, ref);
481
+ }
482
+ historyUpdateSlot(generation) {
483
+ return `history-update:${generation}`;
484
+ }
485
+ async replaceHistoryPin(context, topic, previousGeneration, nextGeneration, ref) {
486
+ const previousSlot = this.historyUpdateSlot(previousGeneration);
487
+ const previous = await this.db
488
+ .collection(MongoCollectionNames.payloadReferences)
489
+ .findOne({
490
+ network: this.scope.network,
491
+ genesisHash: this.scope.genesisHash,
492
+ nodeId: this.scope.nodeId,
493
+ ownerKind: 'basm-job',
494
+ ownerId: topic,
495
+ slot: previousSlot
496
+ }, context.options());
497
+ if (previous !== null) {
498
+ const previousPayload = await this.db
499
+ .collection(MongoCollectionNames.payloads)
500
+ .findOne({ _id: String(previous.payloadId) }, context.options());
501
+ if (previousPayload !== null &&
502
+ isPayloadKind(String(previousPayload.kind)) &&
503
+ typeof previousPayload.digest === 'string') {
504
+ const options = context.options();
505
+ await this.payloads.releaseReference(options.session, {
506
+ scope: this.scope,
507
+ payload: {
508
+ kind: previousPayload.kind,
509
+ digest: previousPayload.digest
510
+ },
511
+ ownerKind: 'basm-job',
512
+ ownerId: topic,
513
+ slot: previousSlot
514
+ }, { timeoutMS: options.timeoutMS, signal: context.signal });
515
+ }
516
+ }
517
+ await this.pin(context, ref, 'basm-job', topic, this.historyUpdateSlot(nextGeneration));
518
+ }
519
+ async applyHandoff(context, decision, expected, nextGeneration) {
520
+ if (!sameScope(expected.scope, this.scope) ||
521
+ expected.topic !== decision.topic ||
522
+ expected.chainEpoch !== decision.expectedHistory.chainEpoch ||
523
+ expected.topicHistoryGeneration !== decision.expectedHistory.topicHistoryGeneration) {
524
+ rejectAdmission('read-conflict');
525
+ }
526
+ const now = new Date();
527
+ const result = await this.jobs().findOneAndUpdate({
528
+ _id: this.jobId(expected),
529
+ topic: expected.topic,
530
+ peerId: expected.peerId,
531
+ jobId: expected.jobId,
532
+ chainEpoch: encodeMongoUint64(expected.chainEpoch),
533
+ topicHistoryGeneration: encodeMongoUint64(expected.topicHistoryGeneration),
534
+ leaseToken: encodeMongoUint64(expected.leaseToken),
535
+ $expr: { $gt: ['$leaseUntil', '$$NOW'] }
536
+ }, {
537
+ $set: {
538
+ topicHistoryGeneration: encodeMongoUint64(nextGeneration),
539
+ checkpoint: decision.historyUpdate?.handoff?.checkpoint ?? expected.jobId,
540
+ updatedAt: now
541
+ }
542
+ }, { ...context.options(), returnDocument: 'after' });
543
+ if (result === null)
544
+ rejectAdmission('read-conflict');
545
+ }
546
+ async upsertTransaction(context, plan) {
547
+ const raw = plan.payloads.find(ref => ref.kind === 'raw-transaction');
548
+ const manifest = plan.payloads.find(ref => ref.kind === 'beef-manifest');
549
+ const now = new Date();
550
+ const id = mongoRecordKey(mongoChainKey(this.scope), 'transaction', plan.identity.txid);
551
+ await this.db.collection(MongoCollectionNames.transactions).updateOne({ _id: id }, {
552
+ $setOnInsert: {
553
+ _id: id,
554
+ schemaVersion,
555
+ network: this.scope.network,
556
+ genesisHash: this.scope.genesisHash,
557
+ txid: plan.identity.txid,
558
+ ...(raw === undefined ? {} : { rawPayloadId: this.payloadId(raw) }),
559
+ ...(manifest === undefined ? {} : { manifestPayloadId: this.payloadId(manifest) }),
560
+ createdAt: now
561
+ },
562
+ $set: { updatedAt: now }
563
+ }, { upsert: true, ...context.options() });
564
+ }
565
+ async insertOutbox(context, kind, intent) {
566
+ const now = new Date();
567
+ try {
568
+ await this.outbox(kind).insertOne({
569
+ _id: this.outboxId(kind, intent.eventId),
570
+ schemaVersion,
571
+ network: this.scope.network,
572
+ genesisHash: this.scope.genesisHash,
573
+ nodeId: this.scope.nodeId,
574
+ eventId: intent.eventId,
575
+ target: intent.target,
576
+ state: 'pending',
577
+ nextAttemptAt: now,
578
+ leaseUntil: new Date(0),
579
+ createdAt: now,
580
+ updatedAt: now
581
+ }, context.options());
582
+ }
583
+ catch (error) {
584
+ if (duplicateKey(error))
585
+ rejectAdmission('invalid-plan');
586
+ throw error;
587
+ }
588
+ for (const [index, payload] of intent.payloads.entries()) {
589
+ await this.pin(context, payload, `${kind}-outbox`, intent.eventId, String(index));
590
+ }
591
+ }
592
+ async pin(context, ref, ownerKind, ownerId, slot) {
593
+ if (!isPayloadKind(ref.kind))
594
+ rejectAdmission('invalid-plan');
595
+ const options = context.options();
596
+ try {
597
+ await this.payloads.addReference(options.session, {
598
+ scope: this.scope,
599
+ payload: { kind: ref.kind, digest: ref.digest },
600
+ ownerKind,
601
+ ownerId,
602
+ slot
603
+ }, { timeoutMS: options.timeoutMS, signal: context.signal });
604
+ }
605
+ catch (error) {
606
+ if (error instanceof Error && error.message === 'Mongo payload is not ready for reference') {
607
+ rejectAdmission('payload-not-ready');
608
+ }
609
+ throw error;
610
+ }
611
+ }
612
+ async outboxPayloads(kind, eventId) {
613
+ const refs = await this.db
614
+ .collection(MongoCollectionNames.payloadReferences)
615
+ .find({
616
+ network: this.scope.network,
617
+ genesisHash: this.scope.genesisHash,
618
+ nodeId: this.scope.nodeId,
619
+ ownerKind: `${kind}-outbox`,
620
+ ownerId: eventId
621
+ })
622
+ .sort({ slot: 1 })
623
+ .toArray();
624
+ const result = [];
625
+ for (const reference of refs) {
626
+ const payload = await this.db
627
+ .collection(MongoCollectionNames.payloads)
628
+ .findOne({ _id: String(reference.payloadId) });
629
+ if (payload === null || !isPayloadKind(payload.kind))
630
+ continue;
631
+ result.push({
632
+ kind: payload.kind,
633
+ digest: payload.digest,
634
+ byteLength: decodeMongoUint64(payload.byteLength)
635
+ });
636
+ }
637
+ return result;
638
+ }
639
+ payloadId(ref) {
640
+ return mongoRecordKey(mongoChainKey(this.scope), ref.kind, ref.digest);
641
+ }
642
+ outputId(topic, outpoint) {
643
+ return mongoRecordKey(mongoNodeKey(this.scope), 'output', topic, outpoint.txid, encodeMongoOutputIndex(outpoint.outputIndex));
644
+ }
645
+ appliedId(topic, txid) {
646
+ return mongoRecordKey(mongoNodeKey(this.scope), 'applied', topic, txid);
647
+ }
648
+ generationId(topic) {
649
+ return mongoRecordKey(mongoNodeKey(this.scope), 'generation', topic);
650
+ }
651
+ jobId(lease) {
652
+ return mongoRecordKey(mongoNodeKey(this.scope), 'job', lease.topic, lease.peerId, lease.jobId);
653
+ }
654
+ outboxId(kind, eventId) {
655
+ return mongoRecordKey(mongoNodeKey(this.scope), `${kind}-outbox`, eventId);
656
+ }
657
+ outputs() {
658
+ return this.db.collection(MongoCollectionNames.outputs);
659
+ }
660
+ applied() {
661
+ return this.db.collection(MongoCollectionNames.appliedTransactions);
662
+ }
663
+ generations() {
664
+ return this.db.collection(MongoCollectionNames.topicGenerations);
665
+ }
666
+ jobs() {
667
+ return this.db.collection(MongoCollectionNames.basmRecoveryJobs);
668
+ }
669
+ outbox(kind) {
670
+ return this.db.collection(kind === 'lookup' ? MongoCollectionNames.lookupOutbox : MongoCollectionNames.propagationOutbox);
671
+ }
672
+ }
673
+ //# sourceMappingURL=MongoAdmissionStorage.js.map