@spine-event-engine/storage-postgres 2.0.0-snapshot.23 → 2.0.0-snapshot.25

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 (53) hide show
  1. package/README.md +34 -16
  2. package/REFERENCE.md +66 -26
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/postgres/agent-history.d.ts +60 -18
  7. package/dist/postgres/agent-history.d.ts.map +1 -1
  8. package/dist/postgres/agent-history.js +114 -37
  9. package/dist/postgres/agent-history.js.map +1 -1
  10. package/dist/postgres/column-mapping.d.ts.map +1 -1
  11. package/dist/postgres/column-mapping.js +6 -3
  12. package/dist/postgres/column-mapping.js.map +1 -1
  13. package/dist/postgres/entity-commit.d.ts +90 -56
  14. package/dist/postgres/entity-commit.d.ts.map +1 -1
  15. package/dist/postgres/entity-commit.js +439 -188
  16. package/dist/postgres/entity-commit.js.map +1 -1
  17. package/dist/postgres/entity-history.d.ts +4 -2
  18. package/dist/postgres/entity-history.d.ts.map +1 -1
  19. package/dist/postgres/entity-history.js +163 -262
  20. package/dist/postgres/entity-history.js.map +1 -1
  21. package/dist/postgres/errors.d.ts +1 -1
  22. package/dist/postgres/errors.d.ts.map +1 -1
  23. package/dist/postgres/errors.js +5 -3
  24. package/dist/postgres/errors.js.map +1 -1
  25. package/dist/postgres/id-column.d.ts.map +1 -1
  26. package/dist/postgres/id-column.js +6 -3
  27. package/dist/postgres/id-column.js.map +1 -1
  28. package/dist/postgres/record-storage.d.ts +228 -117
  29. package/dist/postgres/record-storage.d.ts.map +1 -1
  30. package/dist/postgres/record-storage.js +350 -204
  31. package/dist/postgres/record-storage.js.map +1 -1
  32. package/dist/postgres/storage-factory.d.ts +267 -30
  33. package/dist/postgres/storage-factory.d.ts.map +1 -1
  34. package/dist/postgres/storage-factory.js +786 -145
  35. package/dist/postgres/storage-factory.js.map +1 -1
  36. package/dist/postgres/table-initializer.d.ts +7 -4
  37. package/dist/postgres/table-initializer.d.ts.map +1 -1
  38. package/dist/postgres/table-initializer.js +21 -15
  39. package/dist/postgres/table-initializer.js.map +1 -1
  40. package/dist/postgres/table-resolver.d.ts +15 -1
  41. package/dist/postgres/table-resolver.d.ts.map +1 -1
  42. package/dist/postgres/table-resolver.js +36 -7
  43. package/dist/postgres/table-resolver.js.map +1 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +5 -5
  46. package/dist/postgres/agent-execution.d.ts +0 -284
  47. package/dist/postgres/agent-execution.d.ts.map +0 -1
  48. package/dist/postgres/agent-execution.js +0 -648
  49. package/dist/postgres/agent-execution.js.map +0 -1
  50. package/dist/postgres/delivery-cleanup.d.ts +0 -76
  51. package/dist/postgres/delivery-cleanup.d.ts.map +0 -1
  52. package/dist/postgres/delivery-cleanup.js +0 -224
  53. package/dist/postgres/delivery-cleanup.js.map +0 -1
@@ -11,78 +11,287 @@
11
11
  * or implied. See the License for the specific language governing permissions and limitations under
12
12
  * the License.
13
13
  */
14
- import { StringifierRegistry } from "@spine-event-engine/core";
14
+ import { defaultStringifierRegistry, StringifierRegistry } from "@spine-event-engine/core";
15
15
  import { StorageFactory, } from "@spine-event-engine/storage";
16
16
  import { TenantBoundary, TenantCatalogReads, } from "@spine-event-engine/storage/provider";
17
- import { AgentExecutionRecords, AgentHistoryRecords, } from "@spine-event-engine/storage/provider";
18
- import { DeliveryCleanupStorageFactories, EntityCommitStorageFactories, AgentHistoryStorageFactories, AgentExecutionStorageFactories, } from "@spine-event-engine/storage/provider";
17
+ import { AgentHistoryRecords, AgentModelPreferences } from "@spine-event-engine/storage/provider";
18
+ import { EntityCommitStorageFactories, AgentHistoryStorageFactories, StoragePreparations, stateHistorySpec, eventHistorySpec, eventStoreRecordSpec, EventStoreGroups, } from "@spine-event-engine/storage/provider";
19
19
  import { Pool } from "pg";
20
- import { PostgresStorageConfigurationError, PostgresStorageConnectionError } from "./errors.js";
21
- import { PostgresRecordStorage } from "./record-storage.js";
20
+ import { PostgresStorageConfigurationError, PostgresStorageConnectionError, PostgresStorageSchemaError, } from "./errors.js";
21
+ import { PostgresRecordStorage, } from "./record-storage.js";
22
22
  import { PostgresEntityStorage } from "./entity-history.js";
23
- import { PostgresAgentHistory, AgentHistoryHash } from "./agent-history.js";
24
- import { PostgresAgentExecution } from "./agent-execution.js";
23
+ import { PostgresAgentHistory, AgentHistoryHash, PostgresAgentIndexes } from "./agent-history.js";
25
24
  import { PostgresEntityCommitStorage } from "./entity-commit.js";
26
- import { PostgresDeliveryCleanupStorage } from "./delivery-cleanup.js";
27
25
  import { PostgresTableResolver } from "./table-resolver.js";
28
26
  import { PostgresTableSpecs } from "./table-spec.js";
27
+ import { PostgresTableInitializer } from "./table-initializer.js";
29
28
  /**
30
29
  * Provides the PostgreSQL storage-factory public contract.
31
30
  */
32
31
  export class PostgresStorageFactory extends StorageFactory {
32
+ tenantResolver;
33
33
  resolver;
34
34
  operation;
35
35
  stringifiers;
36
36
  #handles = new Set();
37
- #databases;
37
+ #commits = new Set();
38
+ #databases = new Map();
39
+ #pending = new Map();
40
+ #work = new Set();
41
+ #configs;
42
+ #families = new Map();
43
+ #tenantTargets = new Map();
44
+ #tables = new Map();
45
+ #audits = new Map();
46
+ #bindings = new Map();
47
+ #bindingPending = new Map();
48
+ #sharedCheck;
38
49
  #catalog;
50
+ /**
51
+ * Whether this record family is closed.
52
+ */
39
53
  #closed;
40
54
  /**
41
- * Creates a connected factory from validated database resources.
55
+ * Creates a logical factory from locally validated settings.
42
56
  *
43
- * @param databases Supply the configured tenant databases.
57
+ * @param configs Supply the configured database settings.
58
+ * @param tenantResolver Optionally resolves additional tenant settings.
44
59
  * @param resolver Resolves record families to physical table names.
45
60
  * @param operation Optionally creates custom table SQL.
46
61
  * @param stringifiers Convert message IDs and columns reversibly.
47
62
  */
48
- constructor(databases, resolver, operation, stringifiers) {
63
+ constructor(configs, tenantResolver, resolver, operation, stringifiers) {
49
64
  super();
65
+ this.tenantResolver = tenantResolver;
50
66
  this.resolver = resolver;
51
67
  this.operation = operation;
52
68
  this.stringifiers = stringifiers;
53
- this.#databases = new Map(databases.map((database) => [database.boundary.key, database]));
54
- this.#catalog = new PostgresTenantCatalog(databases.map(({ boundary }) => boundary));
69
+ this.#configs = new Map(configs.map((config) => [config.boundary.key, config]));
70
+ for (const config of configs)
71
+ if (!config.boundary.single)
72
+ this.#tenantTargets.set(config.target, config.boundary.key);
73
+ const tenants = configs.filter(({ boundary }) => !boundary.single);
74
+ this.#catalog = new PostgresTenantCatalog(tenants.map(({ boundary }) => boundary));
75
+ this.registerPorts();
76
+ }
77
+ /**
78
+ * Registers the built-in Entity and Agent provider ports.
79
+ */
80
+ registerPorts() {
81
+ StoragePreparations.register(this, {
82
+ prepareRecord: (context, spec, group, mode) => this.prepareRecord(context, spec, group, mode),
83
+ prepareEntity: (input, mode) => this.prepareEntity(input, mode),
84
+ prepareAgentHistory: (input, mode) => this.prepareAgentHistory(input, mode),
85
+ prepareAgentCommit: (input, mode) => this.prepareAgentCommit(input, mode),
86
+ });
55
87
  EntityCommitStorageFactories.register(this, {
56
88
  createEntityCommitStorage: (input) => this.createEntityCommitStorage(input),
57
89
  });
58
- DeliveryCleanupStorageFactories.register(this, {
59
- createDeliveryCleanupStorage: () => this.createDeliveryCleanupStorage(),
60
- });
61
90
  AgentHistoryStorageFactories.register(this, {
62
91
  createAgentHistoryStorage: (input) => {
63
92
  if (!this.isOpen())
64
93
  throw new Error("StorageFactory is closed.");
65
- return new PostgresAgentHistory(input, this.createPostgresRecordStorage(input.context, AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value)), AgentHistoryRecords.group));
94
+ const spec = AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value));
95
+ return new PostgresAgentHistory(input, this.createPostgresRecordStorage(input.context, spec, AgentHistoryRecords.group), (work) => this.admitPublic(work), (mode) => this.prepareAudit(input.context, spec, mode));
66
96
  },
67
97
  });
68
- AgentExecutionStorageFactories.register(this, {
69
- createAgentExecutionStorage: (input) => this.createAgentExecutionStorage(input),
98
+ }
99
+ /**
100
+ * Admits declarative preparation of one ordinary record family.
101
+ * @typeParam I Record identifier type.
102
+ * @typeParam R Stored record type.
103
+ * @param context Captured storage boundary.
104
+ * @param spec The physical record definition.
105
+ * @param group Optional physical group.
106
+ * @param mode Creation or read-only checking.
107
+ * @returns Completion after compatible preparation.
108
+ */
109
+ prepareRecord(context, spec, group, mode) {
110
+ return this.admitPublic(() => this.prepareRecordAdmitted(context, spec, group, mode));
111
+ }
112
+ /**
113
+ * Prepares a record family within an existing admission.
114
+ * @typeParam I Record identifier type.
115
+ * @typeParam R Stored record type.
116
+ * @param context Captured storage boundary.
117
+ * @param spec The physical record definition.
118
+ * @param group Optional physical group.
119
+ * @param mode Creation or read-only checking.
120
+ * @returns Completion after compatible preparation.
121
+ */
122
+ async prepareRecordAdmitted(context, spec, group, mode) {
123
+ const boundary = this.database(context);
124
+ const name = this.resolver.resolve(spec.sourceType.typeName, group?.name, undefined, spec.recordType.typeName, group === undefined ? undefined : EventStoreGroups.identity(group)).tableName;
125
+ const binding = await this.resolveBinding(boundary);
126
+ const table = this.tableSpec(boundary, name, binding.schema, spec, group);
127
+ const create = this.operation;
128
+ await this.prepareTable(boundary, table, mode, create === undefined ? undefined : () => create(table).sql);
129
+ }
130
+ /**
131
+ * Prepares current Entity and enabled history families from declarations.
132
+ * @typeParam I Entity identifier type.
133
+ * @typeParam S Entity state type.
134
+ * @param input The declarative Entity requirements.
135
+ * @param mode Creation or read-only checking.
136
+ * @returns Completion after all requested families are compatible.
137
+ */
138
+ prepareEntity(input, mode) {
139
+ return this.admitPublic(async () => {
140
+ if (!this.sameEntityColumns(input))
141
+ throw new PostgresStorageConfigurationError("Entity query columns differ from current records.");
142
+ await this.prepareRecordAdmitted(input.context, input.recordSpec, undefined, mode);
143
+ if (input.stateHistory) {
144
+ const history = stateHistorySpec(input.stateSchema);
145
+ await this.prepareRecordAdmitted(input.context, history.spec, history.group, mode);
146
+ }
147
+ if (input.eventHistory) {
148
+ const history = eventHistorySpec(input.stateSchema);
149
+ await this.prepareRecordAdmitted(input.context, history.spec, history.group, mode);
150
+ }
151
+ await this.prepareRecordAdmitted(input.context, eventStoreRecordSpec, EventStoreGroups.forContext(input.context.name), mode);
152
+ });
153
+ }
154
+ /**
155
+ * Compares actual current Entity columns with declared query columns.
156
+ * @typeParam I Entity identifier type.
157
+ * @typeParam S Entity state type.
158
+ * @param input The declarative Entity requirements.
159
+ * @returns Whether physical column declarations agree.
160
+ */
161
+ sameEntityColumns(input) {
162
+ const signature = (columns) => JSON.stringify(columns
163
+ .map((column) => [column.name, PostgresTableSpecs.postgresColumnType(column.type)])
164
+ .sort(([a], [b]) => String(a).localeCompare(String(b))));
165
+ return signature(input.columns) === signature(input.recordSpec.columns);
166
+ }
167
+ /**
168
+ * Prepares the mandatory Agent audit record family and indexes.
169
+ * @typeParam I Agent identifier type.
170
+ * @param input Agent state type and complete tenant scope.
171
+ * @param mode Creation or read-only checking.
172
+ * @returns Completion after table and index compatibility.
173
+ */
174
+ prepareAgentHistory(input, mode) {
175
+ return this.admitPublic(async () => {
176
+ TenantBoundary.of(input.context);
177
+ if (input.stateType.trim().length === 0)
178
+ throw new TypeError("Agent state type is required.");
179
+ const spec = AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value));
180
+ await this.prepareRecordAdmitted(input.context, spec, AgentHistoryRecords.group, mode);
181
+ await this.prepareAudit(input.context, spec, mode);
70
182
  });
71
183
  }
72
184
  /**
73
- * Opens native record families for one typed Agent repository.
74
- *
75
- * @param input Tenant scope, typed ID and generated state layout.
76
- * @returns Closeable Agent execution storage handle.
185
+ * Prepares all live Agent state, audit, and preference requirements without a handle.
186
+ * @param input Declarative Agent Entity layout.
187
+ * @param mode Creation or read-only compatibility check.
188
+ * @returns Completion after native table and audit-index checks.
77
189
  * @typeParam I Typed Agent identifier.
78
190
  * @typeParam S Generated Agent state.
79
191
  */
80
- createAgentExecutionStorage(input) {
81
- if (!this.isOpen())
82
- throw new Error("StorageFactory is closed.");
83
- const context = input.entity.context;
84
- const digest = (value) => AgentHistoryHash.value(value);
85
- return new PostgresAgentExecution(input, this.createPostgresRecordStorage(context, AgentExecutionRecords.invocationSpec(digest), AgentExecutionRecords.invocationGroup), this.createPostgresRecordStorage(context, AgentExecutionRecords.headSpec(digest), AgentExecutionRecords.headGroup), this.createPostgresRecordStorage(context, AgentHistoryRecords.spec(digest), AgentHistoryRecords.group), this.createPostgresRecordStorage(context, input.entity.recordSpec), () => this.createEntityCommitStorage(input.entity));
192
+ prepareAgentCommit(input, mode) {
193
+ return this.admitPublic(async () => {
194
+ if (!this.sameEntityColumns(input))
195
+ throw new PostgresStorageConfigurationError("Entity query columns differ from current records.");
196
+ await this.prepareRecordAdmitted(input.context, input.recordSpec, undefined, mode);
197
+ if (input.stateHistory) {
198
+ const layout = stateHistorySpec(input.stateSchema);
199
+ await this.prepareRecordAdmitted(input.context, layout.spec, layout.group, mode);
200
+ }
201
+ if (input.eventHistory) {
202
+ const layout = eventHistorySpec(input.stateSchema);
203
+ await this.prepareRecordAdmitted(input.context, layout.spec, layout.group, mode);
204
+ }
205
+ await this.prepareRecordAdmitted(input.context, eventStoreRecordSpec, EventStoreGroups.forContext(input.context.name), mode);
206
+ const audit = AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value));
207
+ await this.prepareRecordAdmitted(input.context, audit, AgentHistoryRecords.group, mode);
208
+ await this.prepareAudit(input.context, audit, mode);
209
+ await this.prepareRecordAdmitted(input.context, AgentModelPreferences.spec((value) => AgentHistoryHash.value(value)), AgentModelPreferences.group, mode);
210
+ });
211
+ }
212
+ /**
213
+ * Prepares the three fixed Agent audit indexes for one physical table.
214
+ *
215
+ * @typeParam I Agent identifier type.
216
+ * @typeParam R Audit record type.
217
+ * @param context Captured Agent tenant scope.
218
+ * @param spec Audit record declaration.
219
+ * @param mode Creation or read-only check.
220
+ * @returns Completion after all indexes match.
221
+ */
222
+ async prepareAudit(context, spec, mode) {
223
+ const boundary = this.database(context);
224
+ const binding = await this.resolveBinding(boundary);
225
+ const name = this.resolver.resolve(spec.sourceType.typeName, AgentHistoryRecords.group.name, undefined, spec.recordType.typeName).tableName;
226
+ const key = `${binding.schema}\0${name}`;
227
+ const entries = this.#audits.get(boundary.key) ?? new Map();
228
+ this.#audits.set(boundary.key, entries);
229
+ const current = entries.get(key);
230
+ if (current?.ready)
231
+ return;
232
+ if (current?.pending !== undefined)
233
+ return current.pending;
234
+ return this.startAudit(entries, key, boundary, binding, name, mode);
235
+ }
236
+ /**
237
+ * Starts one Agent index attempt and removes failed evidence for retry.
238
+ *
239
+ * @param entries Audit readiness for the captured boundary.
240
+ * @param key Physical audit table identity.
241
+ * @param boundary Captured Agent tenant scope.
242
+ * @param binding Resolved destination.
243
+ * @param name Physical audit table name.
244
+ * @param mode Creation or read-only check.
245
+ * @returns Completion after index compatibility.
246
+ */
247
+ startAudit(entries, key, boundary, binding, name, mode) {
248
+ const entry = { ready: false, pending: undefined };
249
+ entries.set(key, entry);
250
+ entry.pending = this.attemptAudit(boundary, binding, name, mode)
251
+ .then(() => {
252
+ entry.ready = true;
253
+ }, (error) => {
254
+ entries.delete(key);
255
+ throw error;
256
+ })
257
+ .finally(() => {
258
+ entry.pending = undefined;
259
+ });
260
+ return entry.pending;
261
+ }
262
+ /**
263
+ * Verifies or creates the Agent audit indexes with a released temporary pool when needed.
264
+ *
265
+ * @param boundary Captured Agent tenant scope.
266
+ * @param binding Resolved destination and schema.
267
+ * @param table Physical audit table name.
268
+ * @param mode Creation or read-only check.
269
+ * @returns Completion including temporary pool cleanup.
270
+ */
271
+ async attemptAudit(boundary, binding, table, mode) {
272
+ const runtime = this.#databases.get(boundary.key);
273
+ const pool = runtime?.pool ?? new Pool(binding.config.poolOptions);
274
+ const qualified = `"${binding.schema.replaceAll('"', '""')}"."${table.replaceAll('"', '""')}"`;
275
+ let failure;
276
+ try {
277
+ const client = await pool.connect();
278
+ try {
279
+ await PostgresAgentIndexes.prepare(client, qualified, mode);
280
+ }
281
+ finally {
282
+ client.release();
283
+ }
284
+ }
285
+ catch (error) {
286
+ failure =
287
+ error instanceof Error
288
+ ? error
289
+ : new Error("Agent index preparation failed.", { cause: error });
290
+ }
291
+ if (runtime === undefined)
292
+ await this.closeCheckedPool(pool, failure);
293
+ else if (failure !== undefined)
294
+ throw failure;
86
295
  }
87
296
  /**
88
297
  * Creates a PostgreSQL storage factory builder.
@@ -90,7 +299,7 @@ export class PostgresStorageFactory extends StorageFactory {
90
299
  * @returns A new builder.
91
300
  */
92
301
  static newBuilder() {
93
- return new Builder((entries, resolver, operation, stringifiers) => PostgresStorageFactory.connect(entries, resolver, operation, stringifiers));
302
+ return new Builder((entries, tenantResolver, resolver, operation, stringifiers) => new PostgresStorageFactory(entries, tenantResolver, resolver, operation, stringifiers));
94
303
  }
95
304
  /**
96
305
  * Returns the configured provider tenant catalog.
@@ -100,12 +309,94 @@ export class PostgresStorageFactory extends StorageFactory {
100
309
  tenantCatalog() {
101
310
  return this.#catalog;
102
311
  }
312
+ /**
313
+ * Checks the shared destination without creating tables or retaining a pool.
314
+ *
315
+ * @returns Completion after a read-only schema check.
316
+ */
317
+ checkConnection() {
318
+ if (!this.isOpen())
319
+ return Promise.reject(new Error("StorageFactory is closed."));
320
+ if (this.#sharedCheck !== undefined)
321
+ return this.#sharedCheck;
322
+ const shared = this.#configs.get(TenantBoundary.single.key);
323
+ if (shared === undefined)
324
+ return Promise.reject(new Error("Shared PostgreSQL options are missing."));
325
+ this.#sharedCheck = Promise.resolve()
326
+ .then(() => this.checkShared(shared))
327
+ .catch((error) => {
328
+ this.#sharedCheck = undefined;
329
+ throw error;
330
+ });
331
+ return this.#sharedCheck;
332
+ }
333
+ /**
334
+ * Checks the shared schema using an existing or temporary pool.
335
+ *
336
+ * @param config The shared connection settings.
337
+ * @returns Completion after the read-only probe.
338
+ */
339
+ async checkShared(config) {
340
+ const existing = this.#databases.get(config.boundary.key);
341
+ const pool = existing?.pool ?? new Pool(config.poolOptions);
342
+ let failure;
343
+ try {
344
+ const client = await pool.connect();
345
+ try {
346
+ await PostgresSchemas.resolve(client, config.schema);
347
+ }
348
+ finally {
349
+ client.release();
350
+ }
351
+ }
352
+ catch (error) {
353
+ failure =
354
+ error instanceof PostgresStorageConfigurationError
355
+ ? error
356
+ : new PostgresStorageConnectionError("Unable to connect to PostgreSQL.", {
357
+ cause: error,
358
+ });
359
+ }
360
+ if (existing !== undefined) {
361
+ if (failure !== undefined)
362
+ throw failure;
363
+ return;
364
+ }
365
+ await this.closeCheckedPool(pool, failure);
366
+ }
367
+ /**
368
+ * Closes a temporary check pool without replacing a failed probe.
369
+ *
370
+ * @param pool The temporary pool to dispose.
371
+ * @param failure The classified probe failure, if any.
372
+ * @returns Completion after disposal or a preserved failure.
373
+ */
374
+ async closeCheckedPool(pool, failure) {
375
+ try {
376
+ await pool.end();
377
+ }
378
+ catch (error) {
379
+ const cleanup = new PostgresStorageConnectionError("Unable to close PostgreSQL check pool.", {
380
+ cause: error,
381
+ });
382
+ if (failure !== undefined)
383
+ throw new AggregateError([failure, cleanup], "PostgreSQL connection check and cleanup failed.");
384
+ throw cleanup;
385
+ }
386
+ if (failure !== undefined)
387
+ throw failure;
388
+ }
103
389
  /**
104
390
  * Closes the factory and begins idempotent PostgreSQL pool draining.
391
+ *
392
+ * @returns Completion after handles and the pool close.
105
393
  */
106
394
  close() {
107
- this.#closed ??= this.drain();
108
- void this.#closed.catch(() => undefined);
395
+ this.fenceClose();
396
+ if (this.#closed !== undefined)
397
+ return this.#closed;
398
+ const commits = [...this.#commits].map((handle) => handle.close());
399
+ return (this.#closed = this.drain(commits));
109
400
  }
110
401
  /**
111
402
  * Creates a record-storage handle for the selected PostgreSQL tenant.
@@ -118,7 +409,7 @@ export class PostgresStorageFactory extends StorageFactory {
118
409
  * @returns A live handle bound to the resolved tenant table.
119
410
  */
120
411
  onCreateRecordStorage(context, recordSpec, group) {
121
- return this.createPostgresRecordStorage(context, recordSpec, group);
412
+ return this.createPostgresRecordStorage(context, recordSpec, group, true, true);
122
413
  }
123
414
  /**
124
415
  * Creates one PostgreSQL record handle and tracks it until closure.
@@ -129,22 +420,42 @@ export class PostgresStorageFactory extends StorageFactory {
129
420
  * @param recordSpec Describes the record family.
130
421
  * @param group Optionally separates records sharing a source type.
131
422
  * @returns The live PostgreSQL record handle.
423
+ * @param tracked Whether the factory tracks this public handle.
424
+ * @param publicHandle Whether factory closure fences new work on this handle.
425
+ */
426
+ createPostgresRecordStorage(context, recordSpec, group, tracked = true, publicHandle = false) {
427
+ const boundary = this.database(context);
428
+ const table = this.resolver.resolve(recordSpec.sourceType.typeName, group?.name, undefined, recordSpec.recordType.typeName, group === undefined ? undefined : EventStoreGroups.identity(group));
429
+ const operation = this.operation;
430
+ const handle = new PostgresRecordStorage(context, recordSpec, table.tableName, (schema) => this.tableSpec(boundary, table.tableName, schema, recordSpec, group), this.connections(boundary, publicHandle), () => this.#handles.delete(handle), operation === undefined ? undefined : (resolved) => operation(resolved).sql, this.stringifiers);
431
+ if (tracked)
432
+ this.#handles.add(handle);
433
+ return handle;
434
+ }
435
+ /**
436
+ * Builds a schema-bound layout after checking physical scope separation.
437
+ *
438
+ * @typeParam I The record ID type.
439
+ * @typeParam R The stored record type.
440
+ * @param boundary The captured tenant scope.
441
+ * @param tableName The locally selected table name.
442
+ * @param schema The server-selected schema.
443
+ * @param recordSpec The declared record family.
444
+ * @param group The optional group.
445
+ * @returns The resolved physical layout.
132
446
  */
133
- createPostgresRecordStorage(context, recordSpec, group) {
134
- const database = this.database(context);
135
- const table = this.resolver.resolve(recordSpec.sourceType.typeName, group?.name, undefined, recordSpec.recordType.typeName);
136
- const spec = PostgresTableSpecs.resolvedPostgresTableSpec({
137
- schema: database.schema,
138
- tableName: table.tableName,
447
+ tableSpec(boundary, tableName, schema, recordSpec, group) {
448
+ const table = PostgresTableSpecs.resolvedPostgresTableSpec({
449
+ schema,
450
+ tableName,
139
451
  sourceType: recordSpec.sourceType,
140
452
  recordType: recordSpec.recordType,
141
453
  idType: recordSpec.idType,
142
454
  ...(group === undefined ? {} : { groupName: group.name }),
143
455
  declaredColumns: recordSpec.columns,
144
456
  });
145
- const handle = new PostgresRecordStorage(context, recordSpec, spec, this.connections(database), () => this.#handles.delete(handle), this.operation === undefined ? undefined : createOperation(this.operation, spec), this.stringifiers);
146
- this.#handles.add(handle);
147
- return handle;
457
+ this.assertFamily(boundary, tableName, schema);
458
+ return table;
148
459
  }
149
460
  /**
150
461
  * Creates the internal Entity-current and history storage seam.
@@ -158,77 +469,63 @@ export class PostgresStorageFactory extends StorageFactory {
158
469
  if (!this.isOpen())
159
470
  throw new Error("StorageFactory is closed.");
160
471
  const registration = {};
161
- const handle = new PostgresEntityStorage(input, this.createPostgresRecordStorage(input.context, input.recordSpec), (spec, group) => this.createPostgresRecordStorage(input.context, spec, group), () => this.#handles.delete(registration.handle));
472
+ const handle = new PostgresEntityStorage(input, this.createPostgresRecordStorage(input.context, input.recordSpec), (spec, group) => this.createPostgresRecordStorage(input.context, spec, group), () => this.#handles.delete(registration.handle), (work) => this.admitPublic(work));
162
473
  registration.handle = handle;
163
474
  this.#handles.add(handle);
164
475
  return handle;
165
476
  }
166
477
  /**
167
- * Creates the transaction coordinator for Entity current state and history.
478
+ * Creates independent ordered Entity persistence for current state and history.
168
479
  *
169
480
  * @typeParam I The Entity identifier type.
170
481
  * @typeParam S The Entity state message type.
171
482
  * @param input Supplies the Entity storage configuration.
172
- * @returns A factory-managed commit coordinator.
483
+ * @param admitted Allows a previously admitted Agent operation to open its private commit port.
484
+ * @returns A factory-managed Entity commit handle.
173
485
  */
174
- createEntityCommitStorage(input) {
175
- if (!this.isOpen())
486
+ createEntityCommitStorage(input, admitted = false) {
487
+ if (!admitted && !this.isOpen())
176
488
  throw new Error("StorageFactory is closed.");
177
489
  const registration = {};
178
- const database = this.database(input.context);
179
- const handle = new PostgresEntityCommitStorage(input, (spec, group) => this.createPostgresRecordStorage(input.context, spec, group), this.connections(database), () => this.#handles.delete(registration.handle));
490
+ const handle = new PostgresEntityCommitStorage(input, (spec, group) => this.createPostgresRecordStorage(input.context, spec, group, false), () => this.#commits.delete(registration.handle));
180
491
  registration.handle = handle;
181
- this.#handles.add(handle);
182
- return handle;
183
- }
184
- /**
185
- * Creates the provider's delivered-Inbox cleanup coordinator.
186
- *
187
- * @returns A factory-managed cleanup coordinator.
188
- */
189
- createDeliveryCleanupStorage() {
190
- if (!this.isOpen())
191
- throw new Error("StorageFactory is closed.");
192
- const registration = {};
193
- const handle = new PostgresDeliveryCleanupStorage((context, spec) => this.createPostgresRecordStorage(context, spec), (context) => this.connections(this.database(context)), () => this.#handles.delete(registration.handle));
194
- registration.handle = handle;
195
- this.#handles.add(handle);
492
+ if (!admitted)
493
+ this.#commits.add(handle);
196
494
  return handle;
197
495
  }
198
496
  /**
199
497
  * Closes live handles, the tenant catalog, and all pools.
200
498
  *
201
499
  * @returns A promise that resolves after every pool closes.
500
+ * @param commits The admitted commit completions.
202
501
  */
203
- async drain() {
204
- super.close();
205
- for (const handle of this.#handles)
206
- handle.close();
207
- await this.#catalog.close();
208
- await Promise.all([...this.#databases.values()].map(({ pool }) => pool.end()));
209
- }
210
- /**
211
- * Connects every configured database before exposing a factory.
212
- *
213
- * @param entries Supply validated database configurations.
214
- * @param resolver Resolves physical table names.
215
- * @param operation Optionally creates custom table SQL.
216
- * @param stringifiers Convert message values reversibly.
217
- * @returns The connected factory.
218
- */
219
- static async connect(entries, resolver = new PostgresTableResolver(), operation, stringifiers = new StringifierRegistry()) {
220
- const connected = [];
502
+ async drain(commits) {
503
+ const errors = [];
504
+ for (const result of await Promise.allSettled(commits))
505
+ if (result.status === "rejected")
506
+ errors.push(result.reason);
507
+ await Promise.allSettled([...this.#work, ...this.#pending.values(), this.#sharedCheck]);
508
+ for (const handle of [...this.#handles]) {
509
+ try {
510
+ handle.close();
511
+ }
512
+ catch (error) {
513
+ errors.push(error);
514
+ }
515
+ }
221
516
  try {
222
- for (const entry of entries)
223
- connected.push(await PostgresStorageFactory.prove(entry));
224
- return new PostgresStorageFactory(connected, resolver, operation, stringifiers);
517
+ await this.#catalog.close();
225
518
  }
226
519
  catch (error) {
227
- await Promise.all(connected.map(({ pool }) => pool.end().catch(() => undefined)));
228
- if (error instanceof PostgresStorageConfigurationError)
229
- throw error;
230
- throw new PostgresStorageConnectionError("Unable to connect to PostgreSQL.");
520
+ errors.push(error);
231
521
  }
522
+ for (const result of await Promise.allSettled([...this.#databases.values()].map(({ pool }) => pool.end())))
523
+ if (result.status === "rejected")
524
+ errors.push(result.reason);
525
+ if (errors.length === 1)
526
+ throw errors[0];
527
+ if (errors.length)
528
+ throw new AggregateError(errors, "PostgreSQL factory close failed.");
232
529
  }
233
530
  /**
234
531
  * Opens one pool and proves its selected schema is usable.
@@ -242,8 +539,14 @@ export class PostgresStorageFactory extends StorageFactory {
242
539
  const client = await pool.connect();
243
540
  try {
244
541
  const schema = await PostgresSchemas.resolve(client, entry.schema);
245
- await PostgresSchemas.assertUsable(client, schema);
246
- return { boundary: entry.boundary, pool, schema, databaseName: entry.databaseName };
542
+ return {
543
+ boundary: entry.boundary,
544
+ pool,
545
+ schema,
546
+ target: entry.target,
547
+ databaseName: entry.databaseName,
548
+ config: entry,
549
+ };
247
550
  }
248
551
  finally {
249
552
  client.release();
@@ -255,16 +558,15 @@ export class PostgresStorageFactory extends StorageFactory {
255
558
  }
256
559
  }
257
560
  /**
258
- * Finds the configured database for a storage context.
561
+ * Captures a configured or resolvable tenant boundary.
259
562
  *
260
563
  * @param context Identifies the tenant boundary.
261
- * @returns The matching database resource.
564
+ * @returns The validated boundary.
262
565
  */
263
566
  database(context) {
264
567
  const boundary = TenantBoundary.of(context);
265
- const database = this.#databases.get(boundary.key);
266
- if (database !== undefined)
267
- return database;
568
+ if (this.#configs.has(boundary.key) || (!boundary.single && this.tenantResolver !== undefined))
569
+ return boundary;
268
570
  throw new PostgresStorageConfigurationError(boundary.single
269
571
  ? "PostgreSQL storage is configured for multiple tenants."
270
572
  : "PostgreSQL storage has no configured database for the requested tenant.");
@@ -272,26 +574,352 @@ export class PostgresStorageFactory extends StorageFactory {
272
574
  /**
273
575
  * Exposes record-lifecycle operations for one database resource.
274
576
  *
275
- * @param database Supplies the connected database.
577
+ * @param boundary Identifies the captured tenant scope.
578
+ * @param publicHandle Whether the factory fence rejects new record calls.
276
579
  * @returns Its package-local record lifecycle.
277
580
  */
278
- connections(database) {
581
+ connections(boundary, publicHandle) {
582
+ const database = () => {
583
+ const resolved = this.#databases.get(boundary.key);
584
+ if (resolved === undefined)
585
+ throw new Error("PostgreSQL database is not bound.");
586
+ return resolved;
587
+ };
279
588
  return {
280
- databaseName: database.databaseName,
281
- schema: database.schema,
282
- acquire: () => database.pool.connect(),
589
+ resolve: () => this.resolveDatabase(boundary).then(() => undefined),
590
+ track: (work) => this.track(work),
591
+ prepare: (table, customCreate) => this.prepareTable(boundary, table, "create", customCreate),
592
+ publicOpen: () => !publicHandle || this.isOpen(),
593
+ get databaseName() {
594
+ return database().databaseName;
595
+ },
596
+ get schema() {
597
+ return database().schema;
598
+ },
599
+ acquire: () => database().pool.connect(),
283
600
  };
284
601
  }
602
+ /**
603
+ * Records an admitted provider operation until it settles.
604
+ *
605
+ * @typeParam T The operation result.
606
+ * @param work The admitted promise.
607
+ * @returns The original operation outcome.
608
+ */
609
+ track(work) {
610
+ this.#work.add(work);
611
+ void work.then(() => this.#work.delete(work), () => this.#work.delete(work));
612
+ return work;
613
+ }
614
+ /**
615
+ * Prepares one canonical physical table requirement across handles and explicit preparation.
616
+ *
617
+ * @param boundary Captured storage boundary.
618
+ * @param table Canonical physical table layout.
619
+ * @param mode Creation or read-only compatibility checking.
620
+ * @param customCreate Optional custom create SQL producer.
621
+ * @returns Completion after compatible physical readiness.
622
+ */
623
+ prepareTable(boundary, table, mode, customCreate) {
624
+ const tables = this.#tables.get(boundary.key) ?? new Map();
625
+ this.#tables.set(boundary.key, tables);
626
+ const key = `${table.schema}\0${table.tableName}`;
627
+ const signature = this.tableSignature(table);
628
+ const current = tables.get(key);
629
+ if (current !== undefined && current.signature !== signature)
630
+ return Promise.reject(new PostgresStorageSchemaError("PostgreSQL table definition conflicts."));
631
+ if (current?.ready)
632
+ return Promise.resolve();
633
+ if (current?.pending !== undefined)
634
+ return current.pending;
635
+ return this.startTable(tables, key, signature, boundary, table, mode, customCreate);
636
+ }
637
+ /**
638
+ * Starts one table attempt and removes failed evidence for retry.
639
+ *
640
+ * @param tables Readiness for the captured boundary.
641
+ * @param key Physical table identity.
642
+ * @param signature Canonical layout signature.
643
+ * @param boundary Captured storage boundary.
644
+ * @param table Canonical physical layout.
645
+ * @param mode Creation or read-only check.
646
+ * @param customCreate Optional create SQL producer.
647
+ * @returns Completion after table compatibility.
648
+ */
649
+ startTable(tables, key, signature, boundary, table, mode, customCreate) {
650
+ const entry = { signature, ready: false, pending: undefined };
651
+ tables.set(key, entry);
652
+ const attempt = Promise.resolve().then(() => this.attemptTable(boundary, table, mode, customCreate));
653
+ entry.pending = attempt
654
+ .then(() => {
655
+ entry.ready = true;
656
+ }, (error) => {
657
+ tables.delete(key);
658
+ throw error;
659
+ })
660
+ .finally(() => {
661
+ entry.pending = undefined;
662
+ });
663
+ return entry.pending;
664
+ }
665
+ /**
666
+ * Captures the exact structural declarations for one physical table.
667
+ * @param table Canonical physical table layout.
668
+ * @returns Stable structural signature independent of column input order.
669
+ */
670
+ tableSignature(table) {
671
+ const idType = table.idType;
672
+ return JSON.stringify([
673
+ table.sourceType.typeName,
674
+ table.recordType.typeName,
675
+ typeof idType === "string" ? idType : idType.typeName,
676
+ table.groupName ?? null,
677
+ table.primaryKey,
678
+ [...table.columns]
679
+ .sort((left, right) => left.name.localeCompare(right.name))
680
+ .map((column) => [
681
+ column.name,
682
+ column.postgresType,
683
+ column.nullable,
684
+ column.defaultSql ?? null,
685
+ ]),
686
+ ]);
687
+ }
688
+ /**
689
+ * Prepares one physical table requirement with a released temporary or retained runtime pool.
690
+ *
691
+ * @param boundary Captured storage boundary.
692
+ * @param table Canonical physical table layout.
693
+ * @param mode Creation or read-only compatibility checking.
694
+ * @param customCreate Optional custom create SQL producer.
695
+ * @returns Completion after physical compatibility and cleanup.
696
+ */
697
+ async attemptTable(boundary, table, mode, customCreate) {
698
+ const runtime = this.#databases.get(boundary.key);
699
+ const bound = this.#bindings.get(boundary.key);
700
+ if (bound === undefined)
701
+ throw new Error("PostgreSQL database is not bound.");
702
+ const pool = runtime?.pool ?? new Pool(bound.config.poolOptions);
703
+ let failure;
704
+ try {
705
+ await new PostgresTableInitializer({ acquire: () => pool.connect(), lockIdentity: bound.config.target }, table.schema, table, customCreate).prepare(mode);
706
+ }
707
+ catch (error) {
708
+ failure =
709
+ error instanceof Error
710
+ ? error
711
+ : new Error("PostgreSQL preparation failed.", { cause: error });
712
+ }
713
+ if (runtime === undefined)
714
+ await this.closeCheckedPool(pool, failure);
715
+ else if (failure !== undefined)
716
+ throw failure;
717
+ }
718
+ /**
719
+ * Admits a complete public wrapper operation before invoking application callbacks.
720
+ *
721
+ * @typeParam T The operation result.
722
+ * @param work The public operation to run.
723
+ * @returns The complete admitted outcome.
724
+ */
725
+ admitPublic = (work) => {
726
+ if (!this.isOpen())
727
+ return Promise.reject(new Error("StorageFactory is closed."));
728
+ const { promise, resolve, reject } = Promise.withResolvers();
729
+ void this.track(promise);
730
+ try {
731
+ void work().then(resolve, reject);
732
+ }
733
+ catch (error) {
734
+ reject(error);
735
+ }
736
+ return promise;
737
+ };
738
+ /**
739
+ * Resolves one physical destination once, retrying failed attempts.
740
+ *
741
+ * @param boundary The captured tenant scope.
742
+ * @returns The retained database resource.
743
+ */
744
+ resolveDatabase(boundary) {
745
+ const ready = this.#databases.get(boundary.key);
746
+ if (ready !== undefined)
747
+ return Promise.resolve(ready);
748
+ const pending = this.#pending.get(boundary.key);
749
+ if (pending !== undefined)
750
+ return pending;
751
+ const attempt = Promise.resolve()
752
+ .then(() => this.bindDatabase(boundary))
753
+ .then((database) => {
754
+ this.#databases.set(boundary.key, database);
755
+ return database;
756
+ })
757
+ .finally(() => this.#pending.delete(boundary.key));
758
+ this.#pending.set(boundary.key, attempt);
759
+ return attempt;
760
+ }
761
+ /**
762
+ * Resolves a destination for explicit preparation without retaining a pool.
763
+ * @param boundary Captured storage boundary.
764
+ * @returns Proven connection settings and server-selected schema.
765
+ */
766
+ resolveBinding(boundary) {
767
+ const runtime = this.#databases.get(boundary.key);
768
+ if (runtime !== undefined)
769
+ return Promise.resolve({ config: runtime.config, schema: runtime.schema });
770
+ const bound = this.#bindings.get(boundary.key);
771
+ if (bound !== undefined)
772
+ return Promise.resolve(bound);
773
+ const runtimePending = this.#pending.get(boundary.key);
774
+ if (runtimePending !== undefined)
775
+ return runtimePending.then((database) => ({
776
+ config: database.config,
777
+ schema: database.schema,
778
+ }));
779
+ const pending = this.#bindingPending.get(boundary.key);
780
+ if (pending !== undefined)
781
+ return pending;
782
+ const attempt = this.bindTemporary(boundary)
783
+ .then((result) => {
784
+ this.#bindings.set(boundary.key, result);
785
+ return result;
786
+ })
787
+ .finally(() => this.#bindingPending.delete(boundary.key));
788
+ this.#bindingPending.set(boundary.key, attempt);
789
+ return attempt;
790
+ }
791
+ /**
792
+ * Checks one destination using a temporary pool and releases it on all paths.
793
+ *
794
+ * @param boundary Captured storage boundary.
795
+ * @returns Stable connection settings and schema after cleanup succeeds.
796
+ */
797
+ async bindTemporary(boundary) {
798
+ const config = await this.configFor(boundary);
799
+ const reserved = this.reserveTenantTarget(config);
800
+ const pool = new Pool(config.poolOptions);
801
+ let schema;
802
+ let failure;
803
+ try {
804
+ const client = await pool.connect();
805
+ try {
806
+ schema = await PostgresSchemas.resolve(client, config.schema);
807
+ }
808
+ finally {
809
+ client.release();
810
+ }
811
+ }
812
+ catch (error) {
813
+ failure =
814
+ error instanceof PostgresStorageConfigurationError
815
+ ? error
816
+ : new PostgresStorageConnectionError("Unable to connect to PostgreSQL.", {
817
+ cause: error,
818
+ });
819
+ }
820
+ try {
821
+ await this.closeCheckedPool(pool, failure);
822
+ }
823
+ catch (error) {
824
+ if (reserved)
825
+ this.#tenantTargets.delete(config.target);
826
+ throw error;
827
+ }
828
+ if (schema === undefined)
829
+ throw new Error("PostgreSQL schema was not resolved.");
830
+ return { config, schema };
831
+ }
832
+ /**
833
+ * Resolves settings and proves the selected database without retaining failures.
834
+ *
835
+ * @param boundary The captured tenant scope.
836
+ * @returns The connected database resource.
837
+ */
838
+ async bindDatabase(boundary) {
839
+ const bound = this.#bindings.get(boundary.key) ?? (await this.#bindingPending.get(boundary.key));
840
+ if (bound !== undefined)
841
+ return {
842
+ boundary,
843
+ pool: new Pool(bound.config.poolOptions),
844
+ schema: bound.schema,
845
+ target: bound.config.target,
846
+ databaseName: bound.config.databaseName,
847
+ config: bound.config,
848
+ };
849
+ const config = await this.configFor(boundary);
850
+ const reserved = this.reserveTenantTarget(config);
851
+ try {
852
+ const database = await PostgresStorageFactory.prove(config);
853
+ this.#bindings.set(boundary.key, { config, schema: database.schema });
854
+ return database;
855
+ }
856
+ catch (error) {
857
+ if (reserved)
858
+ this.#tenantTargets.delete(config.target);
859
+ if (error instanceof PostgresStorageConfigurationError)
860
+ throw error;
861
+ throw new PostgresStorageConnectionError("Unable to connect to PostgreSQL.");
862
+ }
863
+ }
864
+ /**
865
+ * Reads configured or application-resolved settings for one captured tenant.
866
+ * @param boundary Captured storage boundary.
867
+ * @returns Validated database settings.
868
+ */
869
+ async configFor(boundary) {
870
+ const fixed = this.#configs.get(boundary.key);
871
+ if (fixed !== undefined)
872
+ return fixed;
873
+ if (boundary.tenantId === undefined || this.tenantResolver === undefined)
874
+ throw new PostgresStorageConfigurationError("PostgreSQL tenant is not configured.");
875
+ const options = await this.tenantResolver(boundary.tenantId);
876
+ return PostgresConfigurations.parse(boundary, options);
877
+ }
878
+ /**
879
+ * Records one physical tenant database before the first connection awaits.
880
+ *
881
+ * @param config The resolved tenant settings.
882
+ * @returns Whether this attempt added the reservation.
883
+ */
884
+ reserveTenantTarget(config) {
885
+ if (config.boundary.single)
886
+ return false;
887
+ const prior = this.#tenantTargets.get(config.target);
888
+ if (prior !== undefined && prior !== config.boundary.key)
889
+ throw new PostgresStorageConfigurationError("PostgreSQL tenants must use distinct physical databases.");
890
+ if (prior !== undefined)
891
+ return false;
892
+ this.#tenantTargets.set(config.target, config.boundary.key);
893
+ return true;
894
+ }
895
+ /**
896
+ * Rejects shared and tenant records mapped to one unpartitioned table.
897
+ *
898
+ * @param boundary The selected storage scope.
899
+ * @param tableName The physical table name.
900
+ * @param schema The resolved server schema.
901
+ */
902
+ assertFamily(boundary, tableName, schema) {
903
+ const target = this.#databases.get(boundary.key)?.target ?? this.#bindings.get(boundary.key)?.config.target;
904
+ if (target === undefined)
905
+ throw new Error("PostgreSQL database is not bound.");
906
+ const key = `${target}\0${schema}\0${tableName}`;
907
+ const prior = this.#families.get(key);
908
+ if (prior !== undefined && prior !== boundary.key)
909
+ throw new PostgresStorageConfigurationError("Shared and tenant PostgreSQL tables collide.");
910
+ this.#families.set(key, boundary.key);
911
+ }
285
912
  }
286
913
  /**
287
914
  * Collects validated options before connecting a PostgreSQL storage factory.
288
915
  */
289
916
  class Builder {
290
- connect;
917
+ assemble;
291
918
  #options;
292
919
  #tenantOptions;
920
+ #tenantResolver;
293
921
  #operationFactory;
294
- #stringifiers = new StringifierRegistry();
922
+ #stringifiers = defaultStringifierRegistry;
295
923
  #resolver = new PostgresTableResolver();
296
924
  /**
297
925
  * Sets the single-database connection options.
@@ -319,6 +947,16 @@ class Builder {
319
947
  }));
320
948
  return this;
321
949
  }
950
+ /**
951
+ * Registers a tenant settings directory without querying it during build.
952
+ *
953
+ * @param resolver Resolves one generated tenant identifier.
954
+ * @returns This builder.
955
+ */
956
+ setTenantResolver(resolver) {
957
+ this.#tenantResolver = resolver;
958
+ return this;
959
+ }
322
960
  /**
323
961
  * Sets custom reversible message stringifiers.
324
962
  *
@@ -326,7 +964,24 @@ class Builder {
326
964
  * @returns This builder.
327
965
  */
328
966
  setStringifierRegistry(registry) {
329
- this.#stringifiers = new StringifierRegistry(registry);
967
+ this.#stringifiers =
968
+ registry === defaultStringifierRegistry ? registry : new StringifierRegistry(registry);
969
+ return this;
970
+ }
971
+ /**
972
+ * Sets the table for one exact domain or system Event Store.
973
+ *
974
+ * @param boundedContextName Logical bounded-context name.
975
+ * @param tableName Physical table name.
976
+ * @param role Domain or system Event Store role.
977
+ * @returns This builder.
978
+ */
979
+ setEventStoreTable(boundedContextName, tableName, role = "domain") {
980
+ const group = EventStoreGroups.forContext(boundedContextName, role);
981
+ const identity = EventStoreGroups.identity(group);
982
+ if (identity === undefined)
983
+ throw new Error("Event Store identity is missing.");
984
+ this.#resolver.setEventStoreName(identity, tableName);
330
985
  return this;
331
986
  }
332
987
  /**
@@ -353,28 +1008,31 @@ class Builder {
353
1008
  return this;
354
1009
  }
355
1010
  /**
356
- * Creates a builder with its factory connection function.
1011
+ * Creates a builder with its local factory construction function.
357
1012
  *
358
- * @param connect Connects validated database configurations.
1013
+ * @param assemble Constructs a logical factory.
359
1014
  */
360
- constructor(connect) {
361
- this.connect = connect;
1015
+ constructor(assemble) {
1016
+ this.assemble = assemble;
362
1017
  }
363
1018
  /**
364
- * Validates the selected mode and connects a factory.
1019
+ * Validates static settings and assembles a logical factory.
365
1020
  *
366
- * @returns The connected PostgreSQL storage factory.
1021
+ * @returns The logical PostgreSQL storage factory without opening a connection.
367
1022
  */
368
1023
  build() {
369
1024
  try {
370
- if (this.#options === undefined && this.#tenantOptions === undefined)
1025
+ if (this.#options === undefined)
371
1026
  throw new PostgresStorageConfigurationError("PostgreSQL storage options are required.");
372
- if (this.#options !== undefined && this.#tenantOptions !== undefined)
373
- throw new PostgresStorageConfigurationError("Configure either single-tenant or multitenant PostgreSQL storage, not both.");
374
- const entries = this.#options === undefined
375
- ? PostgresConfigurations.multitenant(this.#tenantOptions ?? [])
376
- : [PostgresConfigurations.single(this.#options)];
377
- return this.connect(entries, this.#resolver, this.#operationFactory, this.#stringifiers);
1027
+ if (this.#tenantOptions !== undefined && this.#tenantResolver !== undefined)
1028
+ throw new PostgresStorageConfigurationError("Configure either tenant options or a tenant resolver, not both.");
1029
+ const entries = [
1030
+ PostgresConfigurations.single(this.#options),
1031
+ ...(this.#tenantOptions === undefined
1032
+ ? []
1033
+ : PostgresConfigurations.multitenant(this.#tenantOptions)),
1034
+ ];
1035
+ return Promise.resolve(this.assemble(entries, this.#tenantResolver, this.#resolver.copy(), this.#operationFactory, this.#stringifiers));
378
1036
  }
379
1037
  catch (error) {
380
1038
  return Promise.reject(error instanceof Error
@@ -389,10 +1047,19 @@ class Builder {
389
1047
  class PostgresTenantCatalog {
390
1048
  boundaries;
391
1049
  #cursorIdentity = {};
1050
+ /**
1051
+ * Checks fixed configured membership without invoking a resolver.
1052
+ *
1053
+ * @param boundary Complete tenant boundary to inspect.
1054
+ * @returns Whether the boundary is explicitly configured.
1055
+ */
1056
+ contains(boundary) {
1057
+ return Promise.resolve(this.boundaries.some(({ key }) => key === boundary.key));
1058
+ }
392
1059
  /**
393
1060
  * Creates a catalog from configured tenant boundaries.
394
1061
  *
395
- * @param boundaries Supply the configured boundaries.
1062
+ * @param boundaries Supply the enumerable configured boundaries.
396
1063
  */
397
1064
  constructor(boundaries) {
398
1065
  this.boundaries = boundaries;
@@ -611,18 +1278,6 @@ const PostgresConfigurations = Object.freeze({
611
1278
  return value === undefined || (Number.isInteger(value) && value > 0);
612
1279
  },
613
1280
  });
614
- /**
615
- * Adapts a public create-table operation to the record handle callback.
616
- *
617
- * @typeParam I The storage identifier type.
618
- * @typeParam R The stored Protobuf record type.
619
- * @param operation Creates a public operation for a resolved table.
620
- * @param table Supplies the resolved table specification.
621
- * @returns A callback that returns the operation SQL.
622
- */
623
- function createOperation(operation, table) {
624
- return () => operation(table).sql;
625
- }
626
1281
  /**
627
1282
  * Resolves and validates schemas used by connected pools.
628
1283
  */
@@ -655,19 +1310,5 @@ const PostgresSchemas = Object.freeze({
655
1310
  }
656
1311
  return schema;
657
1312
  },
658
- /**
659
- * Rejects schemas that contain the retired storage layout.
660
- *
661
- * @param client Provides a connected client.
662
- * @param schema Names the selected schema.
663
- * @returns A promise that resolves when the schema is usable.
664
- */
665
- async assertUsable(client, schema) {
666
- const result = await client.query("SELECT 1 FROM information_schema.columns WHERE table_schema = $1 " +
667
- "AND LOWER(column_name) IN ('_scope', '_revision') LIMIT 1", [schema]);
668
- if ((result.rowCount ?? 0) > 0) {
669
- throw new PostgresStorageConfigurationError("The configured database contains the retired storage layout.");
670
- }
671
- },
672
1313
  });
673
1314
  //# sourceMappingURL=storage-factory.js.map