@ontrails/core 1.0.0-beta.14 → 1.0.0-beta.15

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 (280) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +17 -13
  4. package/dist/branded.d.ts +1 -1
  5. package/dist/branded.d.ts.map +1 -1
  6. package/dist/branded.js +1 -1
  7. package/dist/branded.js.map +1 -1
  8. package/dist/context.d.ts +13 -1
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/context.js +19 -3
  11. package/dist/context.js.map +1 -1
  12. package/dist/contour.d.ts +81 -0
  13. package/dist/contour.d.ts.map +1 -0
  14. package/dist/contour.js +176 -0
  15. package/dist/contour.js.map +1 -0
  16. package/dist/cross-schema.d.ts +18 -0
  17. package/dist/cross-schema.d.ts.map +1 -0
  18. package/dist/cross-schema.js +29 -0
  19. package/dist/cross-schema.js.map +1 -0
  20. package/dist/draft.d.ts +5 -5
  21. package/dist/draft.d.ts.map +1 -1
  22. package/dist/draft.js +28 -27
  23. package/dist/draft.js.map +1 -1
  24. package/dist/errors.d.ts +62 -4
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +44 -1
  27. package/dist/errors.js.map +1 -1
  28. package/dist/execute.d.ts +26 -9
  29. package/dist/execute.d.ts.map +1 -1
  30. package/dist/execute.js +441 -20
  31. package/dist/execute.js.map +1 -1
  32. package/dist/fire.d.ts +43 -0
  33. package/dist/fire.d.ts.map +1 -0
  34. package/dist/fire.js +185 -0
  35. package/dist/fire.js.map +1 -0
  36. package/dist/index.d.ts +31 -20
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +23 -13
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal/cross-batch.d.ts +44 -0
  41. package/dist/internal/cross-batch.d.ts.map +1 -0
  42. package/dist/internal/cross-batch.js +58 -0
  43. package/dist/internal/cross-batch.js.map +1 -0
  44. package/dist/internal/fork-ctx.d.ts +41 -0
  45. package/dist/internal/fork-ctx.d.ts.map +1 -0
  46. package/dist/internal/fork-ctx.js +47 -0
  47. package/dist/internal/fork-ctx.js.map +1 -0
  48. package/dist/internal/signal-ref.d.ts +15 -0
  49. package/dist/internal/signal-ref.d.ts.map +1 -0
  50. package/dist/internal/signal-ref.js +41 -0
  51. package/dist/internal/signal-ref.js.map +1 -0
  52. package/dist/internal/topo-saves.d.ts +8 -2
  53. package/dist/internal/topo-saves.d.ts.map +1 -1
  54. package/dist/internal/topo-saves.js +162 -23
  55. package/dist/internal/topo-saves.js.map +1 -1
  56. package/dist/internal/topo-snapshots.d.ts +57 -0
  57. package/dist/internal/topo-snapshots.d.ts.map +1 -0
  58. package/dist/internal/topo-snapshots.js +372 -0
  59. package/dist/internal/topo-snapshots.js.map +1 -0
  60. package/dist/internal/topo-store-read.d.ts +23 -20
  61. package/dist/internal/topo-store-read.d.ts.map +1 -1
  62. package/dist/internal/topo-store-read.js +82 -83
  63. package/dist/internal/topo-store-read.js.map +1 -1
  64. package/dist/internal/topo-store.d.ts +19 -5
  65. package/dist/internal/topo-store.d.ts.map +1 -1
  66. package/dist/internal/topo-store.js +174 -124
  67. package/dist/internal/topo-store.js.map +1 -1
  68. package/dist/internal/tracing.d.ts +98 -0
  69. package/dist/internal/tracing.d.ts.map +1 -0
  70. package/dist/internal/tracing.js +102 -0
  71. package/dist/internal/tracing.js.map +1 -0
  72. package/dist/internal/trails-db.d.ts +2 -2
  73. package/dist/internal/trails-db.d.ts.map +1 -1
  74. package/dist/internal/trails-db.js +8 -8
  75. package/dist/internal/trails-db.js.map +1 -1
  76. package/dist/internal/zod-wrappers.d.ts +40 -0
  77. package/dist/internal/zod-wrappers.d.ts.map +1 -0
  78. package/dist/internal/zod-wrappers.js +59 -0
  79. package/dist/internal/zod-wrappers.js.map +1 -0
  80. package/dist/layer.d.ts +22 -5
  81. package/dist/layer.d.ts.map +1 -1
  82. package/dist/layer.js +20 -3
  83. package/dist/layer.js.map +1 -1
  84. package/dist/path-security.d.ts +1 -1
  85. package/dist/path-security.d.ts.map +1 -1
  86. package/dist/path-security.js +1 -1
  87. package/dist/path-security.js.map +1 -1
  88. package/dist/resilience.d.ts +1 -1
  89. package/dist/resilience.d.ts.map +1 -1
  90. package/dist/resilience.js +3 -3
  91. package/dist/resilience.js.map +1 -1
  92. package/dist/resource-config.d.ts +22 -0
  93. package/dist/resource-config.d.ts.map +1 -0
  94. package/dist/resource-config.js +210 -0
  95. package/dist/resource-config.js.map +1 -0
  96. package/dist/resource.d.ts +74 -0
  97. package/dist/resource.d.ts.map +1 -0
  98. package/dist/resource.js +61 -0
  99. package/dist/resource.js.map +1 -0
  100. package/dist/run.d.ts +4 -3
  101. package/dist/run.d.ts.map +1 -1
  102. package/dist/run.js +5 -4
  103. package/dist/run.js.map +1 -1
  104. package/dist/serialization.d.ts.map +1 -1
  105. package/dist/serialization.js +2 -1
  106. package/dist/serialization.js.map +1 -1
  107. package/dist/store/accessor-protocol.d.ts +51 -0
  108. package/dist/store/accessor-protocol.d.ts.map +1 -0
  109. package/dist/store/accessor-protocol.js +2 -0
  110. package/dist/store/accessor-protocol.js.map +1 -0
  111. package/dist/store/index.d.ts +2 -0
  112. package/dist/store/index.d.ts.map +1 -0
  113. package/dist/store/index.js +2 -0
  114. package/dist/store/index.js.map +1 -0
  115. package/dist/surface-filter.d.ts +13 -0
  116. package/dist/surface-filter.d.ts.map +1 -0
  117. package/dist/surface-filter.js +76 -0
  118. package/dist/surface-filter.js.map +1 -0
  119. package/dist/topo-store.d.ts +36 -22
  120. package/dist/topo-store.d.ts.map +1 -1
  121. package/dist/topo-store.js +215 -71
  122. package/dist/topo-store.js.map +1 -1
  123. package/dist/topo.d.ts +23 -9
  124. package/dist/topo.d.ts.map +1 -1
  125. package/dist/topo.js +175 -46
  126. package/dist/topo.js.map +1 -1
  127. package/dist/trail.d.ts +86 -19
  128. package/dist/trail.d.ts.map +1 -1
  129. package/dist/trail.js +59 -4
  130. package/dist/trail.js.map +1 -1
  131. package/dist/trails/derive-trail.d.ts +66 -0
  132. package/dist/trails/derive-trail.d.ts.map +1 -0
  133. package/dist/trails/derive-trail.js +378 -0
  134. package/dist/trails/derive-trail.js.map +1 -0
  135. package/dist/trails/index.d.ts +5 -0
  136. package/dist/trails/index.d.ts.map +1 -0
  137. package/dist/trails/index.js +3 -0
  138. package/dist/trails/index.js.map +1 -0
  139. package/dist/trails/ingest.d.ts +24 -0
  140. package/dist/trails/ingest.d.ts.map +1 -0
  141. package/dist/trails/ingest.js +63 -0
  142. package/dist/trails/ingest.js.map +1 -0
  143. package/dist/transport-error-map.d.ts +106 -0
  144. package/dist/transport-error-map.d.ts.map +1 -0
  145. package/dist/transport-error-map.js +24 -0
  146. package/dist/transport-error-map.js.map +1 -0
  147. package/dist/type-checks.test-d.d.ts +71 -0
  148. package/dist/type-checks.test-d.d.ts.map +1 -0
  149. package/dist/type-checks.test-d.js +12 -0
  150. package/dist/type-checks.test-d.js.map +1 -0
  151. package/dist/type-utils.d.ts +11 -2
  152. package/dist/type-utils.d.ts.map +1 -1
  153. package/dist/type-utils.js.map +1 -1
  154. package/dist/types.d.ts +113 -7
  155. package/dist/types.d.ts.map +1 -1
  156. package/dist/types.js.map +1 -1
  157. package/dist/validate-established-topo.js +2 -2
  158. package/dist/validate-established-topo.js.map +1 -1
  159. package/dist/validate-topo.d.ts.map +1 -1
  160. package/dist/validate-topo.js +24 -7
  161. package/dist/validate-topo.js.map +1 -1
  162. package/dist/workspace.d.ts +1 -1
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +1 -1
  165. package/dist/workspace.js.map +1 -1
  166. package/package.json +8 -2
  167. package/src/__tests__/branded.test.ts +5 -5
  168. package/src/__tests__/context.test.ts +3 -3
  169. package/src/__tests__/contour.test.ts +263 -0
  170. package/src/__tests__/derive-trail.test.ts +668 -0
  171. package/src/__tests__/errors.test.ts +118 -0
  172. package/src/__tests__/execute.test.ts +1179 -141
  173. package/src/__tests__/fire.test.ts +1056 -0
  174. package/src/__tests__/fork-ctx.test.ts +83 -0
  175. package/src/__tests__/ingest.test.ts +191 -0
  176. package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
  177. package/src/__tests__/path-security.test.ts +7 -7
  178. package/src/__tests__/resilience.test.ts +8 -8
  179. package/src/__tests__/resource.test.ts +204 -0
  180. package/src/__tests__/run.test.ts +13 -13
  181. package/src/__tests__/service-config.test.ts +28 -32
  182. package/src/__tests__/surface-filter.test.ts +196 -0
  183. package/src/__tests__/topo-store-read.test.ts +41 -36
  184. package/src/__tests__/topo-store.test.ts +493 -93
  185. package/src/__tests__/topo.test.ts +350 -54
  186. package/src/__tests__/trail.test.ts +189 -24
  187. package/src/__tests__/trails-db.test.ts +32 -25
  188. package/src/__tests__/transport-error-map.test.ts +82 -0
  189. package/src/__tests__/type-utils.test.ts +29 -1
  190. package/src/__tests__/validate-topo.test.ts +147 -25
  191. package/src/__tests__/workspace.test.ts +6 -6
  192. package/src/__tests__/zod-wrappers.test.ts +62 -0
  193. package/src/branded.ts +1 -1
  194. package/src/context.ts +24 -4
  195. package/src/contour.ts +344 -0
  196. package/src/cross-schema.ts +36 -0
  197. package/src/draft.ts +56 -52
  198. package/src/errors.ts +62 -18
  199. package/src/execute.ts +840 -29
  200. package/src/fire.ts +295 -0
  201. package/src/index.ts +116 -29
  202. package/src/internal/cross-batch.ts +77 -0
  203. package/src/internal/fork-ctx.ts +69 -0
  204. package/src/internal/signal-ref.ts +87 -0
  205. package/src/internal/topo-snapshots.ts +517 -0
  206. package/src/internal/topo-store-read.ts +140 -132
  207. package/src/internal/topo-store.ts +291 -186
  208. package/src/internal/tracing.ts +187 -0
  209. package/src/internal/trails-db.ts +9 -11
  210. package/src/internal/zod-wrappers.ts +78 -0
  211. package/src/layer.ts +50 -0
  212. package/src/path-security.ts +1 -1
  213. package/src/resilience.ts +3 -3
  214. package/src/{provision-config.ts → resource-config.ts} +109 -123
  215. package/src/resource.ts +155 -0
  216. package/src/run.ts +5 -4
  217. package/src/serialization.ts +2 -0
  218. package/src/store/accessor-protocol.ts +56 -0
  219. package/src/store/index.ts +4 -0
  220. package/src/surface-filter.ts +182 -0
  221. package/src/topo-store.ts +313 -104
  222. package/src/topo.ts +328 -57
  223. package/src/trail.ts +199 -31
  224. package/src/trails/derive-trail.ts +842 -0
  225. package/src/trails/index.ts +9 -0
  226. package/src/trails/ingest.ts +146 -0
  227. package/src/transport-error-map.ts +56 -0
  228. package/src/type-checks.test-d.ts +106 -0
  229. package/src/type-utils.ts +17 -2
  230. package/src/types.ts +148 -11
  231. package/src/validate-established-topo.ts +2 -2
  232. package/src/validate-topo.ts +32 -8
  233. package/src/workspace.ts +1 -1
  234. package/tsconfig.tests.json +10 -0
  235. package/tsconfig.tsbuildinfo +1 -1
  236. package/dist/adapters.d.ts +0 -39
  237. package/dist/adapters.d.ts.map +0 -1
  238. package/dist/adapters.js +0 -2
  239. package/dist/adapters.js.map +0 -1
  240. package/dist/dispatch.d.ts +0 -27
  241. package/dist/dispatch.d.ts.map +0 -1
  242. package/dist/dispatch.js +0 -34
  243. package/dist/dispatch.js.map +0 -1
  244. package/dist/event.d.ts +0 -8
  245. package/dist/event.d.ts.map +0 -1
  246. package/dist/event.js +0 -7
  247. package/dist/event.js.map +0 -1
  248. package/dist/gate.d.ts +0 -17
  249. package/dist/gate.d.ts.map +0 -1
  250. package/dist/gate.js +0 -21
  251. package/dist/gate.js.map +0 -1
  252. package/dist/health.d.ts +0 -18
  253. package/dist/health.d.ts.map +0 -1
  254. package/dist/health.js +0 -5
  255. package/dist/health.js.map +0 -1
  256. package/dist/job.d.ts +0 -24
  257. package/dist/job.d.ts.map +0 -1
  258. package/dist/job.js +0 -17
  259. package/dist/job.js.map +0 -1
  260. package/dist/provision-config.d.ts +0 -22
  261. package/dist/provision-config.d.ts.map +0 -1
  262. package/dist/provision-config.js +0 -210
  263. package/dist/provision-config.js.map +0 -1
  264. package/dist/provision.d.ts +0 -71
  265. package/dist/provision.d.ts.map +0 -1
  266. package/dist/provision.js +0 -56
  267. package/dist/provision.js.map +0 -1
  268. package/dist/service-config.d.ts +0 -22
  269. package/dist/service-config.d.ts.map +0 -1
  270. package/dist/service-config.js +0 -210
  271. package/dist/service-config.js.map +0 -1
  272. package/dist/service.d.ts +0 -71
  273. package/dist/service.d.ts.map +0 -1
  274. package/dist/service.js +0 -56
  275. package/dist/service.js.map +0 -1
  276. package/src/__tests__/service.test.ts +0 -197
  277. package/src/event.ts +0 -15
  278. package/src/gate.ts +0 -44
  279. package/src/internal/topo-saves.ts +0 -429
  280. package/src/provision.ts +0 -148
package/src/topo-store.ts CHANGED
@@ -1,83 +1,222 @@
1
1
  import type { SQLQueryBindings } from 'bun:sqlite';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, statSync } from 'node:fs';
3
3
 
4
4
  import { NotFoundError } from './errors.js';
5
- import { provision } from './provision.js';
5
+ import type { Topo } from './topo.js';
6
+ import { resource } from './resource.js';
6
7
  import { Result } from './result.js';
7
- import type { TopoPinRecord, TopoSaveRecord } from './internal/topo-saves.js';
8
- import { getTopoPin } from './internal/topo-saves.js';
8
+ import {
9
+ TOPO_SCHEMA_VERSION,
10
+ ensureTopoSnapshotSchema,
11
+ listTopoSnapshots as listStoredTopoSnapshots,
12
+ pinTopoSnapshot as pinStoredTopoSnapshot,
13
+ unpinTopoSnapshot as unpinStoredTopoSnapshot,
14
+ } from './internal/topo-snapshots.js';
15
+ import type {
16
+ CreateTopoSnapshotInput,
17
+ ListTopoSnapshotsOptions,
18
+ TopoSnapshot,
19
+ } from './internal/topo-snapshots.js';
9
20
  import type {
10
21
  TopoStoreExportRecord,
11
- TopoStoreProvisionRecord,
22
+ TopoStoreResourceRecord,
12
23
  TopoStoreRef,
13
24
  TopoStoreTrailDetailRecord,
14
25
  TopoStoreTrailRecord,
15
26
  } from './internal/topo-store-read.js';
16
27
  import {
17
28
  getTopoStoreExport,
18
- getTopoStoreProvision,
29
+ getTopoStoreResource,
19
30
  getTopoStoreTrail,
20
- listTopoStorePins,
21
- listTopoStoreProvisions,
22
- listTopoStoreSaves,
31
+ listTopoStoreResources,
32
+ listTopoStoreSnapshots,
23
33
  listTopoStoreTrails,
24
34
  queryTopoStore,
25
- resolveTopoStoreSave,
35
+ readTopoStoreSnapshot,
26
36
  } from './internal/topo-store-read.js';
27
- import { openReadTrailsDb, resolveTrailsDbPath } from './internal/trails-db.js';
37
+ import { createTopoSnapshot as storeTopoSnapshot } from './internal/topo-store.js';
38
+ import {
39
+ openReadTrailsDb,
40
+ openWriteTrailsDb,
41
+ deriveTrailsDbPath,
42
+ } from './internal/trails-db.js';
28
43
  import type { TrailsDbLocationOptions } from './internal/trails-db.js';
29
44
 
45
+ interface MigratedDbIdentity {
46
+ readonly mtimeMs: number;
47
+ readonly size: number;
48
+ }
49
+
50
+ const migratedTopoDbPaths = new Map<string, MigratedDbIdentity>();
51
+
52
+ /**
53
+ * Test-only instrumentation counters. Incremented by the read-path migration
54
+ * check to let tests assert that a current-schema store does not escalate to
55
+ * a write-mode open, and that cache invalidation re-runs the check when the
56
+ * underlying file is replaced.
57
+ *
58
+ * @internal
59
+ */
60
+ export const __topoStoreMigrationStats = {
61
+ peekCalls: 0,
62
+ writeEscalations: 0,
63
+ };
64
+
65
+ const peekTopoSchemaVersion = (
66
+ options: TrailsDbLocationOptions | undefined
67
+ ): number | undefined => {
68
+ let db: ReturnType<typeof openReadTrailsDb> | undefined;
69
+ try {
70
+ db = openReadTrailsDb(options);
71
+ const row = db
72
+ .query<{ version: number }, [string]>(
73
+ 'SELECT version FROM meta_schema_versions WHERE subsystem = ?'
74
+ )
75
+ .get('topo');
76
+ return row?.version ?? 0;
77
+ } catch {
78
+ // Table missing or unexpected shape: caller will escalate to a write-mode
79
+ // open and run the migration, which rebuilds the schema.
80
+ return undefined;
81
+ } finally {
82
+ db?.close();
83
+ }
84
+ };
85
+
86
+ const statIdentity = (dbPath: string): MigratedDbIdentity | undefined => {
87
+ try {
88
+ const info = statSync(dbPath);
89
+ return { mtimeMs: info.mtimeMs, size: info.size };
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ };
94
+
95
+ const identitiesEqual = (
96
+ a: MigratedDbIdentity,
97
+ b: MigratedDbIdentity
98
+ ): boolean => a.mtimeMs === b.mtimeMs && a.size === b.size;
99
+
100
+ const runTopoMigrationEscalation = (
101
+ options: TrailsDbLocationOptions | undefined,
102
+ dbPath: string,
103
+ fallbackIdentity: MigratedDbIdentity
104
+ ): void => {
105
+ __topoStoreMigrationStats.writeEscalations += 1;
106
+ const db = openWriteTrailsDb(options);
107
+ try {
108
+ ensureTopoSnapshotSchema(db);
109
+ } finally {
110
+ db.close();
111
+ }
112
+ // Re-stat after the migration so the cached identity matches the file we
113
+ // just touched, avoiding a spurious second escalation on the next read.
114
+ const postIdentity = statIdentity(dbPath) ?? fallbackIdentity;
115
+ migratedTopoDbPaths.set(dbPath, postIdentity);
116
+ };
117
+
118
+ const resolveIdentityIfFresh = (
119
+ dbPath: string
120
+ ): MigratedDbIdentity | undefined => {
121
+ if (!existsSync(dbPath)) {
122
+ return undefined;
123
+ }
124
+ const identity = statIdentity(dbPath);
125
+ if (identity === undefined) {
126
+ return undefined;
127
+ }
128
+ const cached = migratedTopoDbPaths.get(dbPath);
129
+ if (cached !== undefined && identitiesEqual(cached, identity)) {
130
+ return undefined;
131
+ }
132
+ return identity;
133
+ };
134
+
135
+ /**
136
+ * Ensure the topo snapshot schema is at the current version before any
137
+ * read-only access. Peeks the version through a read-only handle first and
138
+ * only escalates to a write-mode open + migration when the store is stale.
139
+ *
140
+ * Memoized per resolved DB path keyed on file identity (mtime + size) so a
141
+ * long-running process that deletes and recreates `trails.db` re-runs the
142
+ * migration check against the fresh file.
143
+ *
144
+ * If the DB file does not yet exist, this is a no-op — the downstream read
145
+ * path will surface its own NotFoundError.
146
+ */
147
+ const ensureTopoMigratedIfExists = (
148
+ options?: TrailsDbLocationOptions
149
+ ): void => {
150
+ const dbPath = deriveTrailsDbPath(options);
151
+ const identity = resolveIdentityIfFresh(dbPath);
152
+ if (identity === undefined) {
153
+ return;
154
+ }
155
+
156
+ __topoStoreMigrationStats.peekCalls += 1;
157
+ const version = peekTopoSchemaVersion(options);
158
+ if (version !== undefined && version >= TOPO_SCHEMA_VERSION) {
159
+ // Already current — record identity and skip the write-mode open entirely,
160
+ // preserving the read-only contract for callers whose filesystem mounts
161
+ // `trails.db` read-only.
162
+ migratedTopoDbPaths.set(dbPath, identity);
163
+ return;
164
+ }
165
+
166
+ runTopoMigrationEscalation(options, dbPath, identity);
167
+ };
168
+
30
169
  export type {
31
170
  TopoStoreExportRecord,
32
- TopoStoreProvisionRecord,
171
+ TopoStoreResourceRecord,
33
172
  TopoStoreRef,
34
173
  TopoStoreTrailDetailRecord,
35
174
  TopoStoreTrailRecord,
36
175
  } from './internal/topo-store-read.js';
176
+ export type {
177
+ CreateTopoSnapshotInput,
178
+ ListTopoSnapshotsOptions,
179
+ TopoSnapshot,
180
+ } from './internal/topo-snapshots.js';
37
181
 
38
182
  export interface ReadOnlyTopoStore {
39
183
  readonly exports: {
40
184
  get(ref?: TopoStoreRef): TopoStoreExportRecord | undefined;
41
185
  };
42
- readonly pins: {
43
- get(name: string): TopoPinRecord | undefined;
44
- list(): readonly TopoPinRecord[];
45
- };
46
186
  query<TRow extends Record<string, unknown>>(
47
187
  sql: string,
48
188
  bindings?: readonly SQLQueryBindings[]
49
189
  ): readonly TRow[];
50
- readonly provisions: {
190
+ readonly resources: {
51
191
  get(
52
192
  id: string,
53
- options?: { readonly save?: TopoStoreRef }
54
- ): TopoStoreProvisionRecord | undefined;
193
+ options?: { readonly snapshot?: TopoStoreRef }
194
+ ): TopoStoreResourceRecord | undefined;
55
195
  list(options?: {
56
- readonly save?: TopoStoreRef;
57
- }): readonly TopoStoreProvisionRecord[];
196
+ readonly snapshot?: TopoStoreRef;
197
+ }): readonly TopoStoreResourceRecord[];
58
198
  };
59
- readonly saves: {
60
- get(ref?: TopoStoreRef): TopoSaveRecord | undefined;
61
- latest(): TopoSaveRecord | undefined;
62
- list(): readonly TopoSaveRecord[];
199
+ readonly snapshots: {
200
+ get(ref?: TopoStoreRef): TopoSnapshot | undefined;
201
+ latest(): TopoSnapshot | undefined;
202
+ list(options?: ListTopoSnapshotsOptions): readonly TopoSnapshot[];
63
203
  };
64
204
  readonly trails: {
65
205
  get(
66
206
  id: string,
67
- options?: { readonly save?: TopoStoreRef }
207
+ options?: { readonly snapshot?: TopoStoreRef }
68
208
  ): TopoStoreTrailDetailRecord | undefined;
69
209
  list(options?: {
70
210
  readonly intent?: TopoStoreTrailRecord['intent'];
71
- readonly save?: TopoStoreRef;
211
+ readonly snapshot?: TopoStoreRef;
72
212
  }): readonly TopoStoreTrailRecord[];
73
213
  };
74
214
  }
75
215
 
76
216
  export interface MockTopoStoreSeed {
77
217
  readonly exports?: readonly TopoStoreExportRecord[];
78
- readonly pins?: readonly TopoPinRecord[];
79
- readonly provisions?: readonly TopoStoreProvisionRecord[];
80
- readonly saves?: readonly TopoSaveRecord[];
218
+ readonly resources?: readonly TopoStoreResourceRecord[];
219
+ readonly snapshots?: readonly TopoSnapshot[];
81
220
  readonly trails?: readonly TopoStoreTrailDetailRecord[];
82
221
  }
83
222
 
@@ -90,17 +229,18 @@ const resolveStoreRootDir = (options?: TrailsDbLocationOptions): string =>
90
229
  const requireReadDb = (
91
230
  options?: TrailsDbLocationOptions
92
231
  ): ReturnType<typeof openReadTrailsDb> => {
93
- const dbPath = resolveTrailsDbPath(options);
232
+ const dbPath = deriveTrailsDbPath(options);
94
233
  if (!existsSync(dbPath)) {
95
234
  throw new NotFoundError(missingStoreMessage);
96
235
  }
236
+ ensureTopoMigratedIfExists(options);
97
237
  return openReadTrailsDb(options);
98
238
  };
99
239
 
100
240
  const requireSavedTopoState = (
101
241
  db: ReturnType<typeof openReadTrailsDb>
102
242
  ): void => {
103
- if (resolveTopoStoreSave(db) === undefined) {
243
+ if (readTopoStoreSnapshot(db) === undefined) {
104
244
  throw new NotFoundError(missingStoreMessage);
105
245
  }
106
246
  };
@@ -119,31 +259,26 @@ const withStoredTopoState = <T>(
119
259
  };
120
260
 
121
261
  const createSeedResolver = (seed?: MockTopoStoreSeed) => {
122
- const saves = [...(seed?.saves ?? [])];
123
- const pins = [...(seed?.pins ?? [])];
262
+ const snapshots = [...(seed?.snapshots ?? [])];
124
263
  const trails = [...(seed?.trails ?? [])];
125
- const provisions = [...(seed?.provisions ?? [])];
264
+ const resources = [...(seed?.resources ?? [])];
126
265
  const exports = [...(seed?.exports ?? [])];
127
266
 
128
- const resolveSave = (ref?: TopoStoreRef): TopoSaveRecord | undefined => {
129
- if (ref?.saveId !== undefined) {
130
- return saves.find((save) => save.id === ref.saveId);
267
+ const resolveSnapshot = (ref?: TopoStoreRef): TopoSnapshot | undefined => {
268
+ if (ref?.snapshotId !== undefined) {
269
+ return snapshots.find((snapshot) => snapshot.id === ref.snapshotId);
131
270
  }
132
271
  if (ref?.pin !== undefined) {
133
- const pin = pins.find((candidate) => candidate.name === ref.pin);
134
- return pin === undefined
135
- ? undefined
136
- : saves.find((save) => save.id === pin.saveId);
272
+ return snapshots.find((snapshot) => snapshot.pinnedAs === ref.pin);
137
273
  }
138
- return saves[0];
274
+ return snapshots[0];
139
275
  };
140
276
 
141
277
  return {
142
278
  exports,
143
- pins,
144
- provisions,
145
- resolveSave,
146
- saves,
279
+ resolveSnapshot,
280
+ resources,
281
+ snapshots,
147
282
  trails,
148
283
  };
149
284
  };
@@ -156,69 +291,85 @@ export const createMockTopoStore = (
156
291
  return {
157
292
  exports: {
158
293
  get(ref?: TopoStoreRef) {
159
- const save = resolved.resolveSave(ref);
160
- return save === undefined
294
+ const snapshot = resolved.resolveSnapshot(ref);
295
+ return snapshot === undefined
161
296
  ? undefined
162
- : resolved.exports.find((entry) => entry.save.id === save.id);
297
+ : resolved.exports.find((entry) => entry.snapshot.id === snapshot.id);
163
298
  },
164
299
  },
165
- pins: {
166
- get(name: string) {
167
- return resolved.pins.find((pin) => pin.name === name);
168
- },
169
- list() {
170
- return resolved.pins;
171
- },
300
+ query() {
301
+ throw new NotFoundError(
302
+ 'Mock topoStore.query() is unsupported. Seed typed accessors instead.'
303
+ );
172
304
  },
173
- provisions: {
305
+ resources: {
174
306
  get(id, options) {
175
- const save = resolved.resolveSave(options?.save);
176
- return resolved.provisions.find(
307
+ const snapshot = resolved.resolveSnapshot(options?.snapshot);
308
+ return resolved.resources.find(
177
309
  (item) =>
178
- item.id === id && (save === undefined || item.saveId === save.id)
310
+ item.id === id &&
311
+ (snapshot === undefined || item.snapshotId === snapshot.id)
179
312
  );
180
313
  },
181
314
  list(options) {
182
- const save = resolved.resolveSave(options?.save);
183
- return save === undefined
315
+ const snapshot = resolved.resolveSnapshot(options?.snapshot);
316
+ return snapshot === undefined
184
317
  ? []
185
- : resolved.provisions.filter((item) => item.saveId === save.id);
318
+ : resolved.resources.filter(
319
+ (item) => item.snapshotId === snapshot.id
320
+ );
186
321
  },
187
322
  },
188
- query() {
189
- throw new NotFoundError(
190
- 'Mock topoStore.query() is unsupported. Seed typed accessors instead.'
191
- );
192
- },
193
- saves: {
323
+ snapshots: {
194
324
  get(ref?: TopoStoreRef) {
195
- return resolved.resolveSave(ref);
325
+ return resolved.resolveSnapshot(ref);
196
326
  },
197
327
  latest() {
198
- return resolved.saves[0];
328
+ return resolved.snapshots[0];
199
329
  },
200
- list() {
201
- return resolved.saves;
330
+ list(options) {
331
+ let snapshots =
332
+ options?.pinned === undefined
333
+ ? resolved.snapshots
334
+ : resolved.snapshots.filter((snapshot) =>
335
+ options.pinned
336
+ ? snapshot.pinnedAs !== undefined
337
+ : snapshot.pinnedAs === undefined
338
+ );
339
+ if (options?.before !== undefined) {
340
+ const target = snapshots.find((s) => s.id === options.before);
341
+ if (target !== undefined) {
342
+ snapshots = snapshots.filter(
343
+ (s) =>
344
+ s.createdAt < target.createdAt ||
345
+ (s.createdAt === target.createdAt && s.id < target.id)
346
+ );
347
+ }
348
+ }
349
+ if (options?.limit !== undefined) {
350
+ snapshots = snapshots.slice(0, options.limit);
351
+ }
352
+ return snapshots;
202
353
  },
203
354
  },
204
355
  trails: {
205
356
  get(id, options) {
206
- const save = resolved.resolveSave(options?.save);
207
- if (save === undefined) {
357
+ const snapshot = resolved.resolveSnapshot(options?.snapshot);
358
+ if (snapshot === undefined) {
208
359
  return;
209
360
  }
210
361
  return resolved.trails.find(
211
- (trail) => trail.id === id && trail.saveId === save.id
362
+ (trail) => trail.id === id && trail.snapshotId === snapshot.id
212
363
  );
213
364
  },
214
365
  list(options) {
215
- const save = resolved.resolveSave(options?.save);
216
- if (save === undefined) {
366
+ const snapshot = resolved.resolveSnapshot(options?.snapshot);
367
+ if (snapshot === undefined) {
217
368
  return [];
218
369
  }
219
370
  return resolved.trails.filter(
220
371
  (trail) =>
221
- trail.saveId === save.id &&
372
+ trail.snapshotId === snapshot.id &&
222
373
  (options?.intent === undefined || trail.intent === options.intent)
223
374
  );
224
375
  },
@@ -234,45 +385,39 @@ export const createTopoStore = (
234
385
  return withStoredTopoState(options, (db) => getTopoStoreExport(db, ref));
235
386
  },
236
387
  },
237
- pins: {
238
- get(name: string) {
239
- return withStoredTopoState(options, (db) => getTopoPin(db, name));
240
- },
241
- list() {
242
- return withStoredTopoState(options, (db) => listTopoStorePins(db));
243
- },
388
+ query<TRow extends Record<string, unknown>>(
389
+ sql: string,
390
+ bindings?: readonly SQLQueryBindings[]
391
+ ) {
392
+ return withStoredTopoState(options, (db) =>
393
+ queryTopoStore<TRow>(db, sql, bindings)
394
+ );
244
395
  },
245
- provisions: {
396
+ resources: {
246
397
  get(id, queryOptions) {
247
398
  return withStoredTopoState(options, (db) =>
248
- getTopoStoreProvision(db, id, queryOptions)
399
+ getTopoStoreResource(db, id, queryOptions)
249
400
  );
250
401
  },
251
402
  list(queryOptions) {
252
403
  return withStoredTopoState(options, (db) =>
253
- listTopoStoreProvisions(db, queryOptions)
404
+ listTopoStoreResources(db, queryOptions)
254
405
  );
255
406
  },
256
407
  },
257
- query<TRow extends Record<string, unknown>>(
258
- sql: string,
259
- bindings?: readonly SQLQueryBindings[]
260
- ) {
261
- return withStoredTopoState(options, (db) =>
262
- queryTopoStore<TRow>(db, sql, bindings)
263
- );
264
- },
265
- saves: {
408
+ snapshots: {
266
409
  get(ref?: TopoStoreRef) {
267
410
  return withStoredTopoState(options, (db) =>
268
- resolveTopoStoreSave(db, ref)
411
+ readTopoStoreSnapshot(db, ref)
269
412
  );
270
413
  },
271
414
  latest() {
272
- return withStoredTopoState(options, (db) => resolveTopoStoreSave(db));
415
+ return withStoredTopoState(options, (db) => readTopoStoreSnapshot(db));
273
416
  },
274
- list() {
275
- return withStoredTopoState(options, (db) => listTopoStoreSaves(db));
417
+ list(snapshotOptions) {
418
+ return withStoredTopoState(options, (db) =>
419
+ listTopoStoreSnapshots(db, snapshotOptions)
420
+ );
276
421
  },
277
422
  },
278
423
  trails: {
@@ -289,7 +434,71 @@ export const createTopoStore = (
289
434
  },
290
435
  });
291
436
 
292
- export const topoStore = provision('topo.store', {
437
+ export const createTopoSnapshot = (
438
+ topo: Topo,
439
+ options?: TrailsDbLocationOptions & CreateTopoSnapshotInput
440
+ ): Result<TopoSnapshot, Error> => {
441
+ const db = openWriteTrailsDb(options);
442
+ try {
443
+ return storeTopoSnapshot(db, topo, options);
444
+ } finally {
445
+ db.close();
446
+ }
447
+ };
448
+
449
+ export const listTopoSnapshots = (
450
+ options?: TrailsDbLocationOptions & ListTopoSnapshotsOptions
451
+ ): readonly TopoSnapshot[] => {
452
+ const dbPath = deriveTrailsDbPath(options);
453
+ if (!existsSync(dbPath)) {
454
+ return [];
455
+ }
456
+
457
+ ensureTopoMigratedIfExists(options);
458
+ const db = openReadTrailsDb(options);
459
+ try {
460
+ return listStoredTopoSnapshots(db, options);
461
+ } finally {
462
+ db.close();
463
+ }
464
+ };
465
+
466
+ export const pinTopoSnapshot = (
467
+ id: string,
468
+ name: string,
469
+ options?: TrailsDbLocationOptions
470
+ ): TopoSnapshot | undefined => {
471
+ const dbPath = deriveTrailsDbPath(options);
472
+ if (!existsSync(dbPath)) {
473
+ return undefined;
474
+ }
475
+
476
+ const db = openWriteTrailsDb(options);
477
+ try {
478
+ return pinStoredTopoSnapshot(db, { id, name });
479
+ } finally {
480
+ db.close();
481
+ }
482
+ };
483
+
484
+ export const unpinTopoSnapshot = (
485
+ nameOrId: string,
486
+ options?: TrailsDbLocationOptions
487
+ ): TopoSnapshot | undefined => {
488
+ const dbPath = deriveTrailsDbPath(options);
489
+ if (!existsSync(dbPath)) {
490
+ return undefined;
491
+ }
492
+
493
+ const db = openWriteTrailsDb(options);
494
+ try {
495
+ return unpinStoredTopoSnapshot(db, nameOrId);
496
+ } finally {
497
+ db.close();
498
+ }
499
+ };
500
+
501
+ export const topoStore = resource('topo.store', {
293
502
  create: (svc) =>
294
503
  Result.ok(
295
504
  createTopoStore({