@photon-grove/evt 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +51 -4
  2. package/dist/aggregate.d.ts +26 -0
  3. package/dist/aggregate.d.ts.map +1 -1
  4. package/dist/aggregate.js.map +1 -1
  5. package/dist/dynamodb/head.d.ts +6 -0
  6. package/dist/dynamodb/head.d.ts.map +1 -1
  7. package/dist/dynamodb/head.js +11 -0
  8. package/dist/dynamodb/head.js.map +1 -1
  9. package/dist/dynamodb/index.d.ts +2 -1
  10. package/dist/dynamodb/index.d.ts.map +1 -1
  11. package/dist/dynamodb/index.js +2 -1
  12. package/dist/dynamodb/index.js.map +1 -1
  13. package/dist/dynamodb/repository.d.ts +9 -2
  14. package/dist/dynamodb/repository.d.ts.map +1 -1
  15. package/dist/dynamodb/repository.js +123 -27
  16. package/dist/dynamodb/repository.js.map +1 -1
  17. package/dist/dynamodb/requests.d.ts +37 -3
  18. package/dist/dynamodb/requests.d.ts.map +1 -1
  19. package/dist/dynamodb/requests.js +67 -9
  20. package/dist/dynamodb/requests.js.map +1 -1
  21. package/dist/dynamodb/views.d.ts +18 -0
  22. package/dist/dynamodb/views.d.ts.map +1 -0
  23. package/dist/dynamodb/views.js +56 -0
  24. package/dist/dynamodb/views.js.map +1 -0
  25. package/dist/errors.d.ts +51 -4
  26. package/dist/errors.d.ts.map +1 -1
  27. package/dist/errors.js +60 -4
  28. package/dist/errors.js.map +1 -1
  29. package/dist/index.d.ts +4 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/memory/repository.d.ts +3 -2
  34. package/dist/memory/repository.d.ts.map +1 -1
  35. package/dist/memory/repository.js +78 -29
  36. package/dist/memory/repository.js.map +1 -1
  37. package/dist/postgres/client.d.ts +32 -0
  38. package/dist/postgres/client.d.ts.map +1 -0
  39. package/dist/postgres/client.js +4 -0
  40. package/dist/postgres/client.js.map +1 -0
  41. package/dist/postgres/index.d.ts +4 -0
  42. package/dist/postgres/index.d.ts.map +1 -0
  43. package/dist/postgres/index.js +6 -0
  44. package/dist/postgres/index.js.map +1 -0
  45. package/dist/postgres/repository.d.ts +62 -0
  46. package/dist/postgres/repository.d.ts.map +1 -0
  47. package/dist/postgres/repository.js +598 -0
  48. package/dist/postgres/repository.js.map +1 -0
  49. package/dist/postgres/schema.d.ts +81 -0
  50. package/dist/postgres/schema.d.ts.map +1 -0
  51. package/dist/postgres/schema.js +249 -0
  52. package/dist/postgres/schema.js.map +1 -0
  53. package/dist/repository.d.ts +101 -6
  54. package/dist/repository.d.ts.map +1 -1
  55. package/dist/repository.js +108 -6
  56. package/dist/repository.js.map +1 -1
  57. package/dist/staging.d.ts +28 -0
  58. package/dist/staging.d.ts.map +1 -0
  59. package/dist/staging.js +89 -0
  60. package/dist/staging.js.map +1 -0
  61. package/dist/store.d.ts +15 -3
  62. package/dist/store.d.ts.map +1 -1
  63. package/dist/store.js +38 -54
  64. package/dist/store.js.map +1 -1
  65. package/dist/transaction.d.ts +113 -0
  66. package/dist/transaction.d.ts.map +1 -0
  67. package/dist/transaction.js +207 -0
  68. package/dist/transaction.js.map +1 -0
  69. package/package.json +5 -1
  70. package/src/aggregate.ts +28 -0
  71. package/src/dynamodb/head.ts +15 -0
  72. package/src/dynamodb/index.ts +2 -0
  73. package/src/dynamodb/repository.ts +177 -33
  74. package/src/dynamodb/requests.ts +114 -10
  75. package/src/dynamodb/views.ts +67 -0
  76. package/src/errors.ts +86 -4
  77. package/src/index.ts +19 -6
  78. package/src/memory/repository.ts +93 -32
  79. package/src/postgres/client.ts +37 -0
  80. package/src/postgres/index.ts +23 -0
  81. package/src/postgres/repository.ts +858 -0
  82. package/src/postgres/schema.ts +302 -0
  83. package/src/repository.ts +220 -8
  84. package/src/staging.ts +138 -0
  85. package/src/store.ts +66 -65
  86. package/src/transaction.ts +322 -0
package/src/errors.ts CHANGED
@@ -15,6 +15,10 @@
15
15
  * - `conflict`: optimistic concurrency failed. Another writer committed since the stream was
16
16
  * read. Reload and run the command again.
17
17
  * - `sequenceGap`: a commit or a load skips a sequence. A conflict.
18
+ * - `fenceConflict`: another writer moved a stream the transaction read but didn't write. A
19
+ * conflict, retryable when the engine observed the fenced head.
20
+ * - `retentionConflict`: a transaction fenced a stream whose entity type has retention, which a
21
+ * fence can't protect. Nothing was written.
18
22
  * - `streamHeadDrift`: a stream's head disagrees with its events or snapshot. Never retried.
19
23
  * - `commitTooLarge`: the commit is over the transaction budget. Nothing was written.
20
24
  * - `compactionUncovered`: no snapshot covers the events compaction was asked to delete.
@@ -30,6 +34,8 @@
30
34
  * - `transient`: the backend throttled the write. Retryable. Nothing was written.
31
35
  * - `rebuildInProgress`: a projection rebuild holds the store, so commits wait. Retryable.
32
36
  * - `historyDependentProjection`: compaction was refused for a history-dependent entity type.
37
+ * - `projectorOnlyCommit`: a commit carries projector operations but no events. Nothing was
38
+ * written.
33
39
  * - `replay`: a stored event failed to upcast or decode while an aggregate loaded.
34
40
  * - `snapshot`: a stored snapshot failed to decode while an aggregate loaded.
35
41
  */
@@ -40,6 +46,8 @@ export type EvtErrorCode =
40
46
  | 'invalidCommit'
41
47
  | 'conflict'
42
48
  | 'sequenceGap'
49
+ | 'fenceConflict'
50
+ | 'retentionConflict'
43
51
  | 'streamHeadDrift'
44
52
  | 'commitTooLarge'
45
53
  | 'compactionUncovered'
@@ -51,6 +59,7 @@ export type EvtErrorCode =
51
59
  | 'transient'
52
60
  | 'rebuildInProgress'
53
61
  | 'historyDependentProjection'
62
+ | 'projectorOnlyCommit'
54
63
  | 'replay'
55
64
  | 'snapshot'
56
65
 
@@ -280,6 +289,48 @@ export class RebuildInProgressError extends EvtError {
280
289
  }
281
290
  }
282
291
 
292
+ /** Where a read fence's head came from (ADR 0004): the engine's own read, or the caller. */
293
+ export type FenceProvenance = 'engine' | 'caller'
294
+
295
+ /**
296
+ * A read fence failed: another writer moved a stream the transaction read but didn't write past
297
+ * the head it observed. A fence the engine filled is retryable, since loading again observes the
298
+ * new head. A caller-stamped one isn't: the caller's input is stale. Nothing was written.
299
+ */
300
+ export class FenceConflictError extends EvtError {
301
+ override readonly name = 'FenceConflictError'
302
+ override readonly code = 'fenceConflict'
303
+ readonly entityId: string
304
+ /** The head the transaction fenced the stream at. */
305
+ readonly head: bigint
306
+ readonly provenance: FenceProvenance
307
+
308
+ constructor(entityId: string, head: bigint, provenance: FenceProvenance) {
309
+ super('fenceConflict', `read fence failed for ${entityId} at head ${head} (${provenance})`)
310
+ this.entityId = entityId
311
+ this.head = head
312
+ this.provenance = provenance
313
+ }
314
+ }
315
+
316
+ /**
317
+ * A transaction fenced a stream whose entity type has retention: TTL deletes items in no order, so
318
+ * a fence on a retained stream protects nothing. Nothing was written.
319
+ */
320
+ export class RetentionConflictError extends EvtError {
321
+ override readonly name = 'RetentionConflictError'
322
+ override readonly code = 'retentionConflict'
323
+ readonly entityType: string
324
+
325
+ constructor(entityType: string) {
326
+ super(
327
+ 'retentionConflict',
328
+ `read fences refused for retention-enabled entity type ${entityType}`,
329
+ )
330
+ this.entityType = entityType
331
+ }
332
+ }
333
+
283
334
  /**
284
335
  * Compaction was refused because the store declares the entity's type history-dependent: its
285
336
  * projectors' rebuild replays the events compaction would delete. Nothing was deleted.
@@ -301,6 +352,26 @@ export class HistoryDependentProjectionError extends EvtError {
301
352
  }
302
353
  }
303
354
 
355
+ /**
356
+ * A commit carried projector operations but no events. Views are projections of committed events,
357
+ * so operations with no event behind them are refused on every backend. Nothing was written.
358
+ */
359
+ export class ProjectorOnlyCommitError extends EvtError {
360
+ override readonly name = 'ProjectorOnlyCommitError'
361
+ override readonly code = 'projectorOnlyCommit'
362
+ /** How many projector operations the commit carried. */
363
+ readonly operations: number
364
+
365
+ constructor(operations: number) {
366
+ super(
367
+ 'projectorOnlyCommit',
368
+ `a commit carries ${operations} projector operations but no events; views need a ` +
369
+ 'backing event in the same commit',
370
+ )
371
+ this.operations = operations
372
+ }
373
+ }
374
+
304
375
  /** A load failed closed on a stored event: an upcaster threw, or the payload didn't decode. */
305
376
  export class ReplayError extends EvtError {
306
377
  override readonly name = 'ReplayError'
@@ -337,16 +408,27 @@ export class SnapshotError extends EvtError {
337
408
  }
338
409
  }
339
410
 
340
- /** Whether an error is an optimistic-concurrency conflict: a `conflict` or a `sequenceGap`. */
411
+ /**
412
+ * Whether an error is an optimistic-concurrency conflict: a `conflict`, a `sequenceGap` or a
413
+ * `fenceConflict`.
414
+ */
341
415
  export function isConflict(error: unknown): boolean {
342
- return error instanceof EvtError && (error.code === 'conflict' || error.code === 'sequenceGap')
416
+ return (
417
+ error instanceof EvtError &&
418
+ (error.code === 'conflict' || error.code === 'sequenceGap' || error.code === 'fenceConflict')
419
+ )
343
420
  }
344
421
 
345
422
  /**
346
- * Whether reloading and running the command again may succeed: after a conflict, while a rebuild
347
- * holds the store, after the feature manifest changed under the writer, or after throttling.
423
+ * Whether reloading and running the command again may succeed: after a conflict (a fence the
424
+ * caller stamped excepted), while a rebuild holds the store, after the feature manifest changed
425
+ * under the writer, or after throttling.
348
426
  */
349
427
  export function isRetryable(error: unknown): boolean {
428
+ // A caller-stamped fence stays stale however often the command runs again.
429
+ if (error instanceof FenceConflictError) {
430
+ return error.provenance === 'engine'
431
+ }
350
432
  if (isConflict(error)) {
351
433
  return true
352
434
  }
package/src/index.ts CHANGED
@@ -9,6 +9,8 @@ export {
9
9
  type EventDefinition,
10
10
  type EventDefinitions,
11
11
  type EventOf,
12
+ type Projection,
13
+ type Projector,
12
14
  type Rejection,
13
15
  type Upcaster,
14
16
  defineAggregate,
@@ -31,10 +33,14 @@ export {
31
33
  EvtError,
32
34
  type EvtErrorCode,
33
35
  FeatureManifestChangedError,
36
+ FenceConflictError,
37
+ type FenceProvenance,
34
38
  HistoryDependentProjectionError,
39
+ ProjectorOnlyCommitError,
35
40
  RebuildInProgressError,
36
41
  ReplayError,
37
42
  ReservedEntityIdError,
43
+ RetentionConflictError,
38
44
  SequenceGapError,
39
45
  SnapshotError,
40
46
  StoreNotInitializedError,
@@ -73,12 +79,18 @@ export {
73
79
  emptyMetadata,
74
80
  encodeMetadata,
75
81
  } from './metadata.js'
76
- export type {
77
- CallOptions,
78
- Commit,
79
- NewSnapshot,
80
- Repository,
81
- StoreInitializer,
82
+ export {
83
+ type CallOptions,
84
+ type Commit,
85
+ type NewSnapshot,
86
+ type NormalizedTransaction,
87
+ type ProjectorGroup,
88
+ type ReadFence,
89
+ type Repository,
90
+ type StoreInitializer,
91
+ type TransactionCommit,
92
+ type TransactionalRepository,
93
+ normalizeTransaction,
82
94
  } from './repository.js'
83
95
  export {
84
96
  DEFAULT_RETRY_POLICY,
@@ -87,6 +99,7 @@ export {
87
99
  } from './retry.js'
88
100
  export {MAX_COMMIT_ITEM_BYTES, snapshotCommitSize, snapshotEventsDue} from './snapshots.js'
89
101
  export {compareUtf8, utf8Length} from './utf8.js'
102
+ export {type CallerFence, StreamRead, Transaction, type TransactionResult} from './transaction.js'
90
103
  export {
91
104
  DEFAULT_SNAPSHOT_INTERVAL,
92
105
  type ExecuteOptions,
@@ -7,6 +7,7 @@ import {
7
7
  ConflictError,
8
8
  CompactionUncoveredError,
9
9
  EvtError,
10
+ FenceConflictError,
10
11
  HistoryDependentProjectionError,
11
12
  RebuildInProgressError,
12
13
  SequenceGapError,
@@ -27,11 +28,17 @@ import type {Metadata} from '../metadata.js'
27
28
  import {
28
29
  type CallOptions,
29
30
  type Commit,
30
- type Repository,
31
+ type NormalizedTransaction,
32
+ type ReadFence,
31
33
  type StoreInitializer,
34
+ type TransactionCommit,
35
+ type TransactionalRepository,
32
36
  checkBudget,
37
+ checkFenceRetention,
33
38
  checkNotReserved,
34
- commitSizes,
39
+ normalizeTransaction,
40
+ rankFailures,
41
+ transactionSizes,
35
42
  validateCommit,
36
43
  } from '../repository.js'
37
44
 
@@ -75,7 +82,7 @@ let controlsOf: (repo: MemRepository) => Controls
75
82
  * on its feature manifest, so {@link initialize} is needed only where code treats every backend
76
83
  * alike.
77
84
  */
78
- export class MemRepository implements Repository, StoreInitializer {
85
+ export class MemRepository implements TransactionalRepository, StoreInitializer {
79
86
  readonly #streams = new Map<string, Stream>()
80
87
  readonly #controls: Controls = {manifest: null, rebuild: {generation: 0n, lock: null}}
81
88
 
@@ -95,44 +102,80 @@ export class MemRepository implements Repository, StoreInitializer {
95
102
  options?.signal?.throwIfAborted()
96
103
  validateCommit(commit)
97
104
  checkNotReserved(commit.entityId)
98
- if (commit.events.length === 0) {
99
- return
105
+ if (commit.events.length > 0) {
106
+ this.#write(normalizeTransaction({streams: [commit]}))
100
107
  }
108
+ }
109
+
110
+ async commitTransaction(transaction: TransactionCommit, options?: CallOptions): Promise<void> {
111
+ options?.signal?.throwIfAborted()
112
+ const normalized = normalizeTransaction(transaction)
113
+ if (normalized.streams.length > 0 || normalized.fences.length > 0) {
114
+ this.#write(normalized)
115
+ }
116
+ }
101
117
 
118
+ // Writes a transaction, or refuses it with nothing written, in the order the live backends
119
+ // check: the budget, the marker and the fences' retention, the rebuild lock, then every stream
120
+ // and fence, ranked.
121
+ #write(transaction: NormalizedTransaction): void {
102
122
  // Every backend budgets a commit the same way, so an oversized one fails before any write.
103
- checkBudget(commitSizes(commit, () => undefined))
123
+ checkBudget(transactionSizes(transaction, () => undefined))
104
124
 
105
125
  // The control rows come first, as the two checks that open every live commit: the marker,
106
126
  // then the rebuild lock, which an unsettled generation fails, a force-released one included.
107
127
  this.#checkManifest()
128
+ const retention = this.#controls.manifest?.retention ?? []
129
+ checkFenceRetention(transaction, (entityType) => retention.includes(entityType))
108
130
  const {rebuild} = this.#controls
109
131
  if (rebuild.lock !== null || rebuild.generation % 2n !== 0n) {
110
132
  throw new RebuildInProgressError()
111
133
  }
112
134
 
113
- const held = this.#streams.get(commit.entityId) ?? emptyStream()
114
- checkWrite(commit, held)
115
-
116
- // Copy everything before the first write, so a copy that throws leaves the stream as it was.
117
- const events = commit.events.map(cloneEvent)
118
- const last = events.at(-1)!.sequence
119
- const snapshot = commit.snapshot && {
120
- entityType: commit.snapshot.entityType,
121
- entityId: commit.entityId,
122
- sequence: commit.snapshot.sequence,
123
- eventSequence: last,
124
- payload: copy(commit.snapshot.payload),
135
+ const failures: EvtError[] = []
136
+ for (const stream of transaction.streams) {
137
+ const failure = checkWrite(stream, this.#streams.get(stream.entityId) ?? emptyStream())
138
+ if (failure !== null) {
139
+ failures.push(failure)
140
+ }
125
141
  }
126
-
127
- for (const event of events) {
128
- held.events.set(event.sequence, event)
142
+ for (const fence of transaction.fences) {
143
+ const failure = checkFence(fence, this.#streams.get(fence.entityId) ?? emptyStream())
144
+ if (failure !== null) {
145
+ failures.push(failure)
146
+ }
129
147
  }
130
- held.head = held.head > last ? held.head : last
131
- if (snapshot) {
132
- held.snapshot = snapshot
148
+ const failure = rankFailures(failures)
149
+ if (failure !== undefined) {
150
+ throw failure
133
151
  }
134
152
 
135
- this.#streams.set(commit.entityId, held)
153
+ // Copy everything before the first write, so a copy that throws leaves every stream as it was.
154
+ const writes = transaction.streams.map((commit) => {
155
+ const events = commit.events.map(cloneEvent)
156
+ const last = events.at(-1)!.sequence
157
+ const snapshot = commit.snapshot && {
158
+ entityType: commit.snapshot.entityType,
159
+ entityId: commit.entityId,
160
+ sequence: commit.snapshot.sequence,
161
+ eventSequence: last,
162
+ payload: copy(commit.snapshot.payload),
163
+ }
164
+
165
+ return {entityId: commit.entityId, events, last, snapshot}
166
+ })
167
+
168
+ for (const {entityId, events, last, snapshot} of writes) {
169
+ const held = this.#streams.get(entityId) ?? emptyStream()
170
+ for (const event of events) {
171
+ held.events.set(event.sequence, event)
172
+ }
173
+ held.head = held.head > last ? held.head : last
174
+ if (snapshot) {
175
+ held.snapshot = snapshot
176
+ }
177
+ this.#streams.set(entityId, held)
178
+ }
136
179
  }
137
180
 
138
181
  async events(entityId: string, options?: CallOptions): Promise<SerializedEvent[]> {
@@ -272,29 +315,47 @@ export class MemRepository implements Repository, StoreInitializer {
272
315
  }
273
316
  }
274
317
 
275
- // Refuses a commit the stored stream doesn't accept, as the live backends' conditions do. The
276
- // head update needs no head for a create and the head to be the sequence before the first event
277
- // for an append. Then no event may exist already, and a snapshot must follow the stored one.
278
- function checkWrite(commit: Commit, held: Stream): void {
318
+ // The failure of a commit the stored stream doesn't accept, as the live backends' conditions find
319
+ // it, or null. The head update needs no head for a create and the head to be the sequence before
320
+ // the first event for an append. Then no event may exist already, and a snapshot must follow the
321
+ // stored one.
322
+ function checkWrite(commit: Commit, held: Stream): EvtError | null {
279
323
  const first = commit.events[0]!.sequence
280
324
  const headPasses = first <= 1n ? held.head === 0n : held.head === first - 1n
281
325
  if (!headPasses) {
282
- throw probe(commit.entityId, held, first)
326
+ return probe(commit.entityId, held, first)
283
327
  }
284
328
 
285
329
  // The head update passed and an event put would fail: an event sits above the head.
286
330
  if (commit.events.some((event) => held.events.has(event.sequence))) {
287
- throw drift(commit.entityId, held.head, derivedHead(held))
331
+ return drift(commit.entityId, held.head, derivedHead(held))
288
332
  }
289
333
 
290
334
  const snapshot = commit.snapshot
291
335
  const stored = held.snapshot?.sequence ?? 0n
292
336
  if (snapshot && snapshot.sequence !== stored + 1n) {
293
- throw new ConflictError(
337
+ return new ConflictError(
294
338
  commit.entityId,
295
339
  `snapshot sequence moved: stored ${stored}, commit writes ${snapshot.sequence}`,
296
340
  )
297
341
  }
342
+
343
+ return null
344
+ }
345
+
346
+ // The failure of a fence whose stream's head moved, or null: drift when the head disagrees with
347
+ // the log, and otherwise a fence conflict.
348
+ function checkFence(fence: ReadFence, held: Stream): EvtError | null {
349
+ if (held.head === fence.head) {
350
+ return null
351
+ }
352
+
353
+ const derived = derivedHead(held)
354
+ if (held.head !== derived) {
355
+ return drift(fence.entityId, held.head, derived)
356
+ }
357
+
358
+ return new FenceConflictError(fence.entityId, fence.head, fence.provenance)
298
359
  }
299
360
 
300
361
  // Classifies a failed head update the way the live backends' failure-path probe does: a stored
@@ -0,0 +1,37 @@
1
+ // The PostgreSQL client a PgRepository takes: a structural interface that `pg`'s Pool, Neon's
2
+ // serverless Pool and PGlite satisfy without an adapter, so this package imports no driver.
3
+
4
+ /** The result of one statement: its rows, as the driver returns them. */
5
+ export interface PgResult {
6
+ readonly rows: readonly unknown[]
7
+ }
8
+
9
+ /** Something that runs one statement: `$1`, `$2` and so on in `text` take `values` in order. */
10
+ export interface PgQueryable {
11
+ query(text: string, values?: unknown[]): Promise<PgResult>
12
+ }
13
+
14
+ /**
15
+ * A connection held for one transaction, as a pool lends it. `release` returns it to the pool, and
16
+ * gets the error when the connection may be unusable, so the pool can discard it.
17
+ */
18
+ export interface PgConnection extends PgQueryable {
19
+ release(error?: Error): void
20
+ }
21
+
22
+ /**
23
+ * The client a {@link PgRepository} runs on.
24
+ *
25
+ * - A pool, such as `pg`'s `Pool` or Neon's serverless `Pool`, has `connect`, which lends one
26
+ * connection for each transaction. Single statements outside a transaction go to `query`.
27
+ * - A single connection without `connect`, such as PGlite, runs everything itself, so every
28
+ * repository on it runs one operation at a time on it. Nothing else may use the connection
29
+ * meanwhile: a statement sent inside a repository's transaction would join it.
30
+ *
31
+ * Every value the repository sends is a string or null, with casts in the SQL, and every column it
32
+ * reads is text, so no driver's type parsing touches a stored value. A `pg` `Client` has a
33
+ * `connect` that opens the client rather than lending a connection, so pass a `Pool` instead.
34
+ */
35
+ export interface PgClient extends PgQueryable {
36
+ connect?(): Promise<PgConnection>
37
+ }
@@ -0,0 +1,23 @@
1
+ // @photon-grove/evt/postgres: the PostgreSQL backend. It imports no driver: the repository takes
2
+ // `pg`'s Pool, or anything shaped like it, as a PgClient.
3
+
4
+ export {type PgClient, type PgConnection, type PgQueryable, type PgResult} from './client.js'
5
+ export {POSTGRES_BACKEND, PgRepository, type PgRepositoryOptions} from './repository.js'
6
+
7
+ export {
8
+ DEFAULT_EVENTS_TABLE,
9
+ DEFAULT_SNAPSHOTS_TABLE,
10
+ MAX_EVENTS_TABLE_NAME_BYTES,
11
+ MAX_SNAPSHOTS_TABLE_NAME_BYTES,
12
+ type PgTables,
13
+ RESERVED_INFIX,
14
+ SCHEMA_LOCK,
15
+ entityTypeIndex,
16
+ featuresTable,
17
+ outcomesTable,
18
+ pgTables,
19
+ rebuildGenerationTable,
20
+ rebuildLockTable,
21
+ schemaDdl,
22
+ streamHeadsTable,
23
+ } from './schema.js'