@spine-event-engine/storage-mysql 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 (62) hide show
  1. package/README.md +38 -30
  2. package/REFERENCE.md +68 -59
  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/mysql/agent-history.d.ts +47 -8
  7. package/dist/mysql/agent-history.d.ts.map +1 -1
  8. package/dist/mysql/agent-history.js +85 -19
  9. package/dist/mysql/agent-history.js.map +1 -1
  10. package/dist/mysql/agent-transaction.d.ts +56 -0
  11. package/dist/mysql/agent-transaction.d.ts.map +1 -0
  12. package/dist/mysql/{entity-commit.js → agent-transaction.js} +33 -45
  13. package/dist/mysql/agent-transaction.js.map +1 -0
  14. package/dist/mysql/column-mapping.d.ts +1 -0
  15. package/dist/mysql/column-mapping.d.ts.map +1 -1
  16. package/dist/mysql/column-mapping.js +26 -3
  17. package/dist/mysql/column-mapping.js.map +1 -1
  18. package/dist/mysql/entity-history.d.ts +61 -35
  19. package/dist/mysql/entity-history.d.ts.map +1 -1
  20. package/dist/mysql/entity-history.js +469 -109
  21. package/dist/mysql/entity-history.js.map +1 -1
  22. package/dist/mysql/errors.d.ts +3 -2
  23. package/dist/mysql/errors.d.ts.map +1 -1
  24. package/dist/mysql/errors.js +3 -2
  25. package/dist/mysql/errors.js.map +1 -1
  26. package/dist/mysql/id-column.d.ts +7 -0
  27. package/dist/mysql/id-column.d.ts.map +1 -1
  28. package/dist/mysql/id-column.js +27 -3
  29. package/dist/mysql/id-column.js.map +1 -1
  30. package/dist/mysql/record-storage.d.ts +242 -72
  31. package/dist/mysql/record-storage.d.ts.map +1 -1
  32. package/dist/mysql/record-storage.js +509 -291
  33. package/dist/mysql/record-storage.js.map +1 -1
  34. package/dist/mysql/storage-factory.d.ts +286 -36
  35. package/dist/mysql/storage-factory.d.ts.map +1 -1
  36. package/dist/mysql/storage-factory.js +1294 -217
  37. package/dist/mysql/storage-factory.js.map +1 -1
  38. package/dist/mysql/table-resolver.d.ts +32 -1
  39. package/dist/mysql/table-resolver.d.ts.map +1 -1
  40. package/dist/mysql/table-resolver.js +81 -23
  41. package/dist/mysql/table-resolver.js.map +1 -1
  42. package/dist/mysql/table-spec.d.ts +0 -4
  43. package/dist/mysql/table-spec.d.ts.map +1 -1
  44. package/dist/mysql/table-spec.js +1 -33
  45. package/dist/mysql/table-spec.js.map +1 -1
  46. package/dist/mysql/testing.d.ts +8 -0
  47. package/dist/mysql/testing.d.ts.map +1 -1
  48. package/dist/mysql/testing.js +23 -2
  49. package/dist/mysql/testing.js.map +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +5 -5
  52. package/dist/mysql/agent-execution.d.ts +0 -278
  53. package/dist/mysql/agent-execution.d.ts.map +0 -1
  54. package/dist/mysql/agent-execution.js +0 -638
  55. package/dist/mysql/agent-execution.js.map +0 -1
  56. package/dist/mysql/delivery-cleanup.d.ts +0 -37
  57. package/dist/mysql/delivery-cleanup.d.ts.map +0 -1
  58. package/dist/mysql/delivery-cleanup.js +0 -108
  59. package/dist/mysql/delivery-cleanup.js.map +0 -1
  60. package/dist/mysql/entity-commit.d.ts +0 -79
  61. package/dist/mysql/entity-commit.d.ts.map +0 -1
  62. package/dist/mysql/entity-commit.js.map +0 -1
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # MySQL storage for Spine TS
2
2
 
3
+ See [storage layout and database preparation](https://github.com/SpineEventEngine/spine-ts/blob/master/docs/STORAGE_LAYOUT.md) for
4
+ physical structures, tenant placement, prerequisites, and customization.
5
+
3
6
  `@spine-event-engine/storage-mysql` stores Spine TS records in MySQL. It is a
4
7
  durable implementation of `@spine-event-engine/storage` and manages the mysql2
5
8
  connection pool and its private tables. PostgreSQL is not supported by this
@@ -11,14 +14,15 @@ record schemas, and a reachable MySQL database.
11
14
  For database requirements, query limits, lifecycle, and error details, see
12
15
  [REFERENCE documentation for agents](REFERENCE.md).
13
16
 
14
- For an application using Agents, this provider keeps the Agent's accepted
15
- work and recorded interactions across restarts. Use InnoDB for Agent repositories;
17
+ For an application using Agents, this provider retains typed admission receipts,
18
+ Entity state, and recorded interactions across restarts, without resuming an
19
+ interrupted handler. Use InnoDB for Agent repositories;
16
20
  see the [coding-agent reference](REFERENCE.md) for database requirements.
17
21
 
18
22
  ## 💡 Why use it?
19
23
 
20
24
  - ✅ Stores Spine records durably in a MySQL database.
21
- - ✅ Manages a bounded `mysql2` connection pool; InnoDB writes are transactional.
25
+ - ✅ Manages a bounded `mysql2` connection pool and atomic single-record conditions.
22
26
  - ✅ Supports declared-column filters, sorting, offsets, limits, and
23
27
  continuations.
24
28
  - ✅ Keeps adapter tables private behind the common `RecordStorage` API.
@@ -38,9 +42,12 @@ The snapshot tag can change before a stable release.
38
42
 
39
43
  ## 🔌 Create and close a MySQL factory
40
44
 
41
- Provide a MySQL URL that includes a database name. Building the factory opens
42
- its pool only; each record family creates and verifies its private table lazily
43
- on first use.
45
+ Provide a MySQL URL that includes a database name. Building the factory
46
+ validates settings locally and opens no pool. `checkConnection()` optionally
47
+ probes the shared database with a read-only query. It reuses an existing
48
+ runtime pool or opens and releases a temporary pool. The first storage
49
+ operation binds its database and creates or verifies
50
+ its private table lazily.
44
51
 
45
52
  <!-- docs-snippet-path: packages/storage-mysql/src/index.ts -->
46
53
 
@@ -74,7 +81,7 @@ try {
74
81
  if (stored?.value !== "task-42") throw new Error("The record was not stored.");
75
82
  } finally {
76
83
  await records.close();
77
- factory.close();
84
+ await factory.close();
78
85
  }
79
86
  ```
80
87
 
@@ -83,8 +90,9 @@ require TLS and provide the issuing CA explicitly; do not enable certificate
83
90
  verification without the CA material needed by your managed MySQL endpoint.
84
91
 
85
92
  For a multitenant application, assign each complete generated `TenantId` to a
86
- different database. The factory creates one pool per configured tenant and
87
- selects that pool before it opens a table or starts a transaction:
93
+ different database. The factory resolves each tenant's database and opens
94
+ its pool on first use; concurrent first uses share the binding. It selects
95
+ that pool before opening a table or starting a transaction:
88
96
 
89
97
  ```ts
90
98
  import { create } from "@bufbuild/protobuf";
@@ -95,6 +103,7 @@ const acme = create(TenantIdSchema, { kind: { case: "value", value: "acme" } });
95
103
  const globex = create(TenantIdSchema, { kind: { case: "value", value: "globex" } });
96
104
 
97
105
  const factory = await MysqlStorageFactory.newBuilder()
106
+ .setOptions({ url: "mysql://user:password@db/shared" })
98
107
  .setTenantOptions([
99
108
  { tenantId: acme, options: { url: "mysql://user:password@db/acme" } },
100
109
  { tenantId: globex, options: { url: "mysql://user:password@db/globex" } },
@@ -102,6 +111,12 @@ const factory = await MysqlStorageFactory.newBuilder()
102
111
  .build();
103
112
  ```
104
113
 
114
+ Alternatively, `setTenantResolver(async (tenantId) => options)` resolves each
115
+ tenant's destination on first use. Fixed tenant entries and a resolver cannot
116
+ be combined. Either mode requires shared `setOptions`; tenants must use
117
+ distinct databases. Shared and tenant settings may point to the same database
118
+ only when their physical table families do not collide.
119
+
105
120
  Use a dedicated database account. The account must be able to create and
106
121
  inspect the adapter tables and perform normal reads and writes. Do not commit
107
122
  connection URLs or credentials.
@@ -118,13 +133,16 @@ pnpm typecheck:build
118
133
  ## ✨ Supported records and queries
119
134
 
120
135
  The adapter performs record CRUD, payload-based compare-and-set, named-column
121
- queries, sort order, offsets, limits, and keyset continuations. InnoDB makes
122
- `writeAll` and Entity commits transactional. MyISAM and Aria instead use
123
- deterministic ordered writes with immutable-prefix retry semantics; a failed
124
- write may require an identical retry. It supports primitive and structured
136
+ queries, sort order, offsets, limits, and keyset continuations. Batches and
137
+ Entity commits use independent ordered records on InnoDB, MyISAM, and Aria.
138
+ A failed operation can leave its completed prefix; an identical Entity retry
139
+ reuses prepared versions and Event IDs. It supports primitive and structured
125
140
  storage IDs. Declared columns support strings, booleans, integral Protobuf
126
- numbers, enums, bytes, messages, `Timestamp`, and `Version`. Spine JVM JDBC
127
- does not support `float` or `double` record columns, so this adapter rejects
141
+ numbers, enums, bytes, messages, `Timestamp`, and `Version`. The provider-only
142
+ live Agent commit is a separate InnoDB transaction: it conditionally checks
143
+ the initial Entity version and complete model preferences before committing
144
+ any changed state, final audit, and preferences together. Spine JVM JDBC does
145
+ not support `float` or `double` record columns, so this adapter rejects
128
146
  them too.
129
147
 
130
148
  Indexed `Timestamp` columns store epoch nanoseconds in signed 64-bit `BIGINT`.
@@ -228,21 +246,11 @@ TLS, and plan backups, monitoring, and migrations. Indexed `bigint` values must
228
246
  fit the exact signed 64-bit MySQL range. PostgreSQL is provided separately by
229
247
  [`@spine-event-engine/storage-postgres`](https://github.com/SpineEventEngine/spine-ts/blob/master/packages/storage-postgres/README.md).
230
248
 
231
- Before starting this corrected layout against an existing database, run the
232
- legacy-layout inventory for every configured tenant database:
233
-
234
- ```sh
235
- pnpm --dir packages/storage-mysql inventory:legacy -- \
236
- --url 'mysql://user:password@127.0.0.1:3306/tenant_a' \
237
- --url 'mysql://user:password@127.0.0.1:3306/tenant_b'
238
- ```
239
-
240
- The command exits nonzero if a database is unreachable or contains the old
241
- `_scope`, `_revision`, or compound scope key. Do not start the corrected
242
- runtime until it passes. The factory repeats this check for every configured
243
- database before it becomes usable. The runtime does not read both layouts or choose
244
- between rows that old Bounded Context partitions would collapse onto the same
245
- new `ID`; migrate those rows offline and stop on every conflict.
249
+ Factory construction validates configuration without connecting. Explicit preparation
250
+ or first use checks the selected record families; it does not scan unrelated
251
+ tables. Existing tables must satisfy the expected layout. See the
252
+ [storage layout reference](https://github.com/SpineEventEngine/spine-ts/blob/master/docs/STORAGE_LAYOUT.md#6-startup-sequence) for
253
+ creation and compatibility-check timing.
246
254
 
247
255
  ## 🔗 Learn more
248
256
 
package/REFERENCE.md CHANGED
@@ -5,6 +5,7 @@ This reference is for coding agents configuring the supported MySQL storage adap
5
5
  ## Public entry point
6
6
 
7
7
  Import `MysqlStorageFactory`, `MysqlStorageOptions`, `MysqlTenantStorageOptions`,
8
+ `MysqlTenantResolver`,
8
9
  `MysqlEntityStorageHandle`, `MysqlStorageFactoryBuilder`,
9
10
  `MysqlCreateOperation`, `CreateOperationFactory`, `MysqlTableSpec`,
10
11
  `MysqlColumnSpec`, `MysqlStorageConfigurationError`,
@@ -15,12 +16,27 @@ Import `MysqlStorageFactory`, `MysqlStorageOptions`, `MysqlTenantStorageOptions`
15
16
  ## Connection and schema
16
17
 
17
18
  `MysqlStorageFactory.newBuilder().setOptions(options).build()` configures one
18
- single-tenant database. `setTenantOptions(entries)` configures one distinct
19
- database and pool per complete generated `TenantId`. The factory selects
20
- the matching pool before table, query, transaction, or lock work.
21
- `setStringifierRegistry(registry)` configures reversible mappings for
22
- message-valued IDs and ordinary message columns; the builder snapshots the
23
- registry so later caller changes do not alter stored or queried values.
19
+ shared database without connecting. `setTenantOptions(entries)` configures
20
+ distinct databases for complete generated `TenantId` values;
21
+ `setTenantResolver(resolver)` selects them asynchronously on first use. Fixed
22
+ entries and a resolver are mutually exclusive. Shared options are required in
23
+ both modes. The first storage operation binds each database and its pool;
24
+ concurrent first uses share that binding, while a failed binding can retry.
25
+ `checkConnection()` probes only the shared database with `SELECT 1`, performs
26
+ no schema checks or DDL, and releases a temporary pool unless runtime work
27
+ already bound one. Successful checks are remembered; failed checks can retry.
28
+ The provider-only `StoragePreparations` port accepts declared Record, current
29
+ Entity and enabled history, or surviving Agent audit requirements. Its `check`
30
+ mode reads physical metadata without creating tables or indexes. Its `create`
31
+ mode creates missing structures and verifies their complete layout. Both modes
32
+ reuse a bound runtime pool or release temporary pools, and compatible results
33
+ are shared with later ordinary use. A failed attempt remains retryable.
34
+ The default shared registry converts message-valued IDs and ordinary message
35
+ columns using generated descriptors registered on module import.
36
+ `setStringifierRegistry(registry)` accepts a custom registry; the builder
37
+ snapshots it so later caller changes do not alter stored or queried values.
38
+ `setEventStoreTable(contextName, table, role?)` selects one physical Event Store
39
+ table for the exact bounded-context name and domain or system role.
24
40
  `Identifiers` choose the storage ID representation; the configured
25
41
  `StringifierRegistry` supplies the matching reversible representation for
26
42
  message-valued IDs and declared message columns. `RecordQuery<I>` statically
@@ -28,7 +44,8 @@ types IDs only. Filter names and values are runtime inputs: MySQL checks the
28
44
  name against the declared columns and maps each value through the corresponding
29
45
  descriptor/column mapping, rather than accepting an application-constructed
30
46
  provider string.
31
- Per-family tables are created and verified lazily on first use. Options support
47
+ Without explicit preparation, per-family tables are created and verified lazily
48
+ on first use. Options support
32
49
  `connectionLimit`, `connectTimeoutMs`, and TLS material. Failure to validate
33
50
  configuration throws `MysqlStorageConfigurationError`; inaccessible connections
34
51
  are reported as `MysqlStorageConnectionError`; an
@@ -45,34 +62,36 @@ one-byte binary collation, such as `latin1_bin`, and can store only IDs
45
62
  representable in that character set. An `utf8mb4` ID of that declared width
46
63
  cannot be converted to MyISAM.
47
64
 
48
- Before deployment, run `pnpm --dir packages/storage-mysql inventory:legacy --
49
- --url <database-url>` once for every configured tenant database. The command
50
- fails closed on connection errors, `_scope`, `_revision`, or an old primary key
51
- containing `_scope`. Passing this inventory is a startup prerequisite for an
52
- upgraded application. Factory build repeats the inspection for every configured
53
- database; the runtime has no dual-layout reader or automatic
54
- conflict winner.
65
+ The optional `inventory:legacy` command remains available for operators
66
+ examining older layouts. Factory build and `checkConnection()` do not scan
67
+ unrelated tables or perform an upgrade-layout inventory. Requested record
68
+ families are checked when normal storage operations first use them.
55
69
 
56
70
  ## Lifecycle and tenancy
57
71
 
58
- `close(): void` marks the factory and its live handles closed and starts pool
59
- draining. Callers do not await it. A record handle closes independently. A
60
- single-tenant factory has one database. A multitenant factory requires a
61
- complete generated `TenantId` on each operation and routes it to its configured
62
- database. Bounded Context names are diagnostic only and never partition rows,
63
- tables, locks, or queries.
64
-
65
- Each Entity source and history family has an independent table. Errors do not
66
- include connection URLs, credentials, or provider details.
72
+ `close(): Promise<void>` immediately fences new public operations, waits for
73
+ complete admitted record, Entity, Agent, and commit work, then closes live
74
+ handles and drains pools. Await it to
75
+ observe cleanup failures; repeated calls return the same completion. A record
76
+ handle closes independently. Shared operations use the configured shared
77
+ destination. Creating a multitenant storage handle requires a complete
78
+ generated `TenantId` in its `StorageContext`; that captured tenant selects the
79
+ database for operations through the handle. Bounded Context names are
80
+ diagnostic only and never partition rows, tables, locks, or queries.
81
+
82
+ Each Entity source and history family has an independent table. Top-level
83
+ provider error messages and public responses omit connection URLs,
84
+ credentials, and driver details. The original driver failure remains available
85
+ as the internal `cause`; do not expose it in public responses or unfiltered
86
+ logs.
67
87
 
68
88
  ## Records and queries
69
89
 
70
- CRUD operations use normal statements. Payload-based compare-and-set uses an
71
- InnoDB transaction or a keyed `GET_LOCK` on MyISAM and Aria. On InnoDB,
72
- `writeAll` encodes the input first, applies entries in input order, and commits
73
- all rows or none. On MyISAM and Aria, `writeAll` has deterministic input-order
74
- semantics but is not transactional; a failed Entity prefix requires an
75
- identical retry. Repeated storage slots in a batch leave the last entry as the
90
+ CRUD operations use normal statements. Payload-based compare-and-set uses
91
+ atomic conditional statements for one storage slot on InnoDB, MyISAM, and Aria.
92
+ `writeAll` encodes input first and writes entries in input order; a later failure
93
+ leaves completed rows in place on every engine. Repeated storage slots in a
94
+ batch leave the last completed entry as the
76
95
  stored value. IDs are declared as generated message IDs or the supported
77
96
  primitive `string`, `int32`, and `int64` kinds. Primitive IDs use native MySQL
78
97
  values; message IDs use their reversible stringifier, compact Proto JSON by
@@ -121,17 +140,18 @@ acquired.
121
140
  ## Entity history seam
122
141
 
123
142
  The provider/framework Entity handle returns current records plus immutable
124
- state and event history. It is not a remote application history API. State append and retention for one entity use a MySQL server lock;
125
- this coordination applies to one MySQL server only. Trim and truncate use
143
+ state and event history. It is not a remote application history API. State and
144
+ diagnostic append use immutable per-record insertion. Trim and truncate use
126
145
  fixed-size chunks. A failed chunk can leave earlier chunks durable, so callers
127
146
  retry maintenance. Current-record and history actions are separate calls, not
128
147
  one cross-storage transaction.
129
148
 
130
- The internal atomic Entity commit port writes current state, configured history,
131
- and framework delivery events. InnoDB uses one connection transaction;
132
- nontransactional engines use a deterministic immutable-history prefix and may
133
- require an identical retry after a storage failure. Standalone history
134
- operations remain separate.
149
+ The internal Entity commit port writes state history, diagnostic Events,
150
+ framework delivery Events, then current state as independent records. A failure
151
+ can leave a completed prefix on every engine. One classified deadlock retry
152
+ replays captured input with unchanged versions and Event IDs. Standalone
153
+ history operations remain separate; a delivery Event alone does not prove the
154
+ current-state write completed.
135
155
 
136
156
  ## Agent history
137
157
 
@@ -150,26 +170,15 @@ misordered, or otherwise incompatible same-name indexes reject when the handle
150
170
  opens. Pages use bounded native SQL chunks and sum serialized entry bytes; a
151
171
  fetched chunk can be larger than the returned byte budget.
152
172
 
153
- ## Agent execution
154
-
155
- Agent execution conformance is exercised with MySQL 5.7 and 8.4, and MariaDB 11.4.
156
-
157
- Agent repositories require InnoDB. The execution handle rejects a nontransactional
158
- engine before accepting Agent work; the existing engine choices for other Entity
159
- families are unchanged. Execution and per-instance pending records use complete
160
- binary ordering keys and validated composite indexes.
161
-
162
- One connection transaction checks the claim and initial Entity Version, then
163
- writes state, mandatory history, preferences and outgoing signals. Connection
164
- binding is scoped to that asynchronous operation. Encoded execution,
165
- per-instance, and Agent history records must each fit the 65,535-byte BLOB limit. The runtime must
166
- check its response reservation against the provider limits before inference.
167
- Reopening a factory can read the saved work. No transaction remains open during
168
- a model request.
169
-
170
- `MysqlStorageSchemaError` reports incompatible layouts,
171
- `MysqlStorageDataError` reports stored bytes that cannot be decoded, and
172
- `MysqlStorageOperationError` reports sanitized bounds, query, collision, and
173
- driver-operation failures. Custom creation uses `useOperationFactory()`; its
174
- resolved `MysqlTableSpec` has the same native column types and naming precedence
175
- as default DDL (explicit grouped name, explicit record name, then default).
173
+ ## Agent live persistence
174
+
175
+ The live Agent path is exercised on MySQL and MariaDB. Agent repositories
176
+ require transactional InnoDB tables for atomic Agent commits. The handle
177
+ rejects a nontransactional engine before accepting Agent work; other Entity
178
+ families retain their configured engine behavior. `appendAll()` commits an
179
+ immutable Agent history group in one transaction. `commitAgent()` compares
180
+ the exact Entity image and model preferences, then writes accepted state,
181
+ preference, and history changes atomically. A no-op or preference-only turn
182
+ can leave the Entity Version unchanged. The separate typed receipt records
183
+ admission, not executable work for replay. Connection binding is scoped to an
184
+ asynchronous operation; no transaction stays open during a model request.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { MysqlStorageConfigurationError, MysqlStorageConnectionError, MysqlStorageDataError, MysqlStorageFactory, MysqlStorageOperationError, MysqlStorageSchemaError, type CreateOperationFactory, type MysqlColumnSpec, type MysqlCreateOperation, type MysqlStorageFactoryBuilder, type MysqlStorageOptions, type MysqlTenantStorageOptions, type MysqlTableSpec, } from "./mysql/storage-factory.js";
1
+ export { MysqlStorageConfigurationError, MysqlStorageConnectionError, MysqlStorageDataError, MysqlStorageFactory, MysqlStorageOperationError, MysqlStorageSchemaError, type CreateOperationFactory, type MysqlColumnSpec, type MysqlCreateOperation, type MysqlStorageFactoryBuilder, type MysqlStorageOptions, type MysqlTenantStorageOptions, type MysqlTenantResolver, type MysqlTableSpec, } from "./mysql/storage-factory.js";
2
2
  export type { MysqlEntityStorageHandle } from "./mysql/entity-history.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,8BAA8B,EAC9B,2BAA2B,EAC3B,qBAAqB,EACrB,mBAAmB,EACnB,0BAA0B,EAC1B,uBAAuB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,GACpB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,8BAA8B,EAC9B,2BAA2B,EAC3B,qBAAqB,EACrB,mBAAmB,EACnB,0BAA0B,EAC1B,uBAAuB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,cAAc,GACpB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,8BAA8B,EAC9B,2BAA2B,EAC3B,qBAAqB,EACrB,mBAAmB,EACnB,0BAA0B,EAC1B,uBAAuB,GAQxB,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,8BAA8B,EAC9B,2BAA2B,EAC3B,qBAAqB,EACrB,mBAAmB,EACnB,0BAA0B,EAC1B,uBAAuB,GASxB,MAAM,4BAA4B,CAAC"}
@@ -1,7 +1,8 @@
1
1
  import type { AgentHistoryEntry } from "@spine-event-engine/proto/agent";
2
- import type { AgentHistoryRecord } from "@spine-event-engine/proto/generated/spine/server/agent/history_record_pb.js";
3
- import { type AgentHistoryPage, type AgentHistoryRead, type AgentHistoryStorage, type AgentHistoryStorageInput } from "@spine-event-engine/storage/provider";
4
- import { MysqlRecordStorage } from "./record-storage.js";
2
+ import { type AgentHistoryRecord } from "@spine-event-engine/proto/generated/spine/server/agent/history_record_pb.js";
3
+ import { type AgentHistoryCapacity, type AgentHistoryPage, type AgentHistoryRead, type AgentHistoryStorage, type AgentHistoryStorageInput } from "@spine-event-engine/storage/provider";
4
+ import { MysqlRecordStorage, type OnPublicOperation } from "./record-storage.js";
5
+ import { MysqlAgentTransaction } from "./agent-transaction.js";
5
6
  /**
6
7
  * Hashes complete identities into bounded physical IDs and indexed scope keys.
7
8
  */
@@ -14,6 +15,10 @@ export declare const AgentHistoryHash: {
14
15
  */
15
16
  value(value: string): string;
16
17
  };
18
+ /**
19
+ * Declares the complete native indexes required by MySQL Agent history.
20
+ */
21
+ export declare const AgentHistoryIndexes: readonly [readonly ["agent_history_full", readonly ["scope_digest", "order_key"]], readonly ["agent_history_category", readonly ["scope_digest", "category", "order_key"]], readonly ["agent_history_conversation", readonly ["scope_digest", "conversation_digest", "order_key"]]];
17
22
  /**
18
23
  * Stores Agent history in the shared MySQL immutable record family.
19
24
  *
@@ -23,13 +28,36 @@ export declare class MysqlAgentHistory<Id> implements AgentHistoryStorage<Id> {
23
28
  #private;
24
29
  private readonly input;
25
30
  private readonly records;
31
+ private readonly onAdmit;
32
+ private readonly transaction?;
33
+ /**
34
+ * Maximum immutable Agent history record bytes supported by MySQL.
35
+ */
36
+ readonly capacity: AgentHistoryCapacity;
26
37
  /**
27
38
  * Binds one typed Agent repository to the provider record handle.
28
39
  *
29
40
  * @param input Repository and complete tenant scope.
30
41
  * @param records Native record-family storage.
42
+ * @param onAdmit Admits complete public history operations.
43
+ * @param transaction Coordinates native immutable audit groups.
44
+ */
45
+ constructor(input: AgentHistoryStorageInput<Id>, records: MysqlRecordStorage<string, AgentHistoryRecord>, onAdmit?: OnPublicOperation, transaction?: MysqlAgentTransaction | undefined);
46
+ /**
47
+ * Writes atomically writes captured immutable rows in the selected tenant database.
48
+ * @param entityId Typed Agent identifier.
49
+ * @param entries Complete Agent audit group.
50
+ * @returns Completion after the native transaction commits.
51
+ */
52
+ appendAll(entityId: Id, entries: readonly AgentHistoryEntry[]): Promise<void>;
53
+ /**
54
+ * Performs one admitted Agent audit group on a native connection.
55
+ *
56
+ * @param entityId Typed Entity identifier.
57
+ * @param entries Complete immutable history group.
58
+ * @returns Completion after the complete immutable audit group commits atomically.
31
59
  */
32
- constructor(input: AgentHistoryStorageInput<Id>, records: MysqlRecordStorage<string, AgentHistoryRecord>);
60
+ private appendAllAdmitted;
33
61
  /**
34
62
  * Writes one original entry or confirms byte-identical content.
35
63
  *
@@ -38,6 +66,14 @@ export declare class MysqlAgentHistory<Id> implements AgentHistoryStorage<Id> {
38
66
  * @returns Completion after immutable storage confirms the entry.
39
67
  */
40
68
  append(entityId: Id, entry: AgentHistoryEntry): Promise<void>;
69
+ /**
70
+ * Executes an admitted provider operation.
71
+ *
72
+ * @param entityId Typed Agent identifier.
73
+ * @param entry Original entry.
74
+ * @returns Completion after immutable storage.
75
+ */
76
+ private appendAdmitted;
41
77
  /**
42
78
  * Reads a bounded indexed page in full Timestamp/category/UTF-8 order.
43
79
  *
@@ -46,9 +82,14 @@ export declare class MysqlAgentHistory<Id> implements AgentHistoryStorage<Id> {
46
82
  */
47
83
  read(request: AgentHistoryRead<Id>): Promise<AgentHistoryPage>;
48
84
  /**
49
-
85
+ * Executes an admitted provider operation.
86
+ *
87
+ * @param request Typed history page request.
88
+ * @returns Original entries and continuation status.
89
+ */
90
+ private readAdmitted;
91
+ /**
50
92
  * Closes this handle without deleting stored records.
51
-
52
93
  */
53
94
  close(): void;
54
95
  /**
@@ -95,9 +136,7 @@ export declare class MysqlAgentHistory<Id> implements AgentHistoryStorage<Id> {
95
136
  */
96
137
  private key;
97
138
  /**
98
-
99
139
  * Rejects calls after this handle closes.
100
-
101
140
  */
102
141
  private requireOpen;
103
142
  }
@@ -1 +1 @@
1
- {"version":3,"file":"agent-history.d.ts","sourceRoot":"","sources":["../../src/mysql/agent-history.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6EAA6E,CAAC;AACtH,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC9B,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGzD;;GAEG;AACH,eAAO,MAAM,gBAAgB;IAC3B;;;;;OAKG;iBACU,MAAM,GAAG,MAAM;CAG7B,CAAC;AAEF;;;;GAIG;AACH,qBAAa,iBAAiB,CAAC,EAAE,CAAE,YAAW,mBAAmB,CAAC,EAAE,CAAC;;IAoBjE,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B;;;;;OAKG;gBAEgB,KAAK,EAAE,wBAAwB,CAAC,EAAE,CAAC,EACnC,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,CAAC;IAG1E;;;;;;OAMG;IACG,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAYnE;;;;;OAKG;IACG,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAYpE;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAMb;;;;OAIG;IACH,OAAO,CAAC,KAAK;IAIb;;;;OAIG;YACW,UAAU;IAexB;;;;;;;;OAQG;YACW,KAAK;IA4BnB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAkBrB;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAQpB;;;;;OAKG;IACH,OAAO,CAAC,GAAG;IAMX;;;;OAIG;IACH,OAAO,CAAC,WAAW;CAGpB"}
1
+ {"version":3,"file":"agent-history.d.ts","sourceRoot":"","sources":["../../src/mysql/agent-history.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,6EAA6E,CAAC;AACrF,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC9B,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAG/D;;GAEG;AACH,eAAO,MAAM,gBAAgB;IAC3B;;;;;OAKG;iBACU,MAAM,GAAG,MAAM;CAG7B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,qRAItB,CAAC;AAEX;;;;GAIG;AACH,qBAAa,iBAAiB,CAAC,EAAE,CAAE,YAAW,mBAAmB,CAAC,EAAE,CAAC;;IAyBjE,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IA3B/B;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAiD;IAYxF;;;;;;;OAOG;gBAEgB,KAAK,EAAE,wBAAwB,CAAC,EAAE,CAAC,EACnC,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACvD,OAAO,GAAE,iBAAoC,EAC7C,WAAW,CAAC,EAAE,qBAAqB,GAAG,SAAS;IAGlE;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,iBAAiB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7E;;;;;;OAMG;YACW,iBAAiB;IA+B/B;;;;;;OAMG;IACG,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAInE;;;;;;OAMG;YACW,cAAc;IAY5B;;;;;OAKG;IACG,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIpE;;;;;OAKG;YACW,YAAY;IAY1B;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;;;OAIG;IACH,OAAO,CAAC,KAAK;IAOb;;;;OAIG;YACW,UAAU;IAMxB;;;;;;;;OAQG;YACW,KAAK;IA4BnB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAkBrB;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAQpB;;;;;OAKG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,OAAO,CAAC,WAAW;CAGpB"}
@@ -12,9 +12,12 @@
12
12
  * the License.
13
13
  */
14
14
  import { createHash } from "node:crypto";
15
+ import { clone, toBinary } from "@bufbuild/protobuf";
16
+ import { AgentHistoryRecordSchema, } from "@spine-event-engine/proto/generated/spine/server/agent/history_record_pb.js";
15
17
  import { AgentHistoryKeys, AgentHistoryPages, } from "@spine-event-engine/storage/provider";
16
18
  import { AgentHistoryRecords } from "@spine-event-engine/storage/provider";
17
19
  import { MysqlRecordStorage } from "./record-storage.js";
20
+ import { MysqlAgentTransaction } from "./agent-transaction.js";
18
21
  import { AgentHistoryIndexBytes } from "./table-spec.js";
19
22
  /**
20
23
  * Hashes complete identities into bounded physical IDs and indexed scope keys.
@@ -30,6 +33,14 @@ export const AgentHistoryHash = {
30
33
  return createHash("sha256").update(value, "utf8").digest("hex");
31
34
  },
32
35
  };
36
+ /**
37
+ * Declares the complete native indexes required by MySQL Agent history.
38
+ */
39
+ export const AgentHistoryIndexes = [
40
+ ["agent_history_full", ["scope_digest", "order_key"]],
41
+ ["agent_history_category", ["scope_digest", "category", "order_key"]],
42
+ ["agent_history_conversation", ["scope_digest", "conversation_digest", "order_key"]],
43
+ ];
33
44
  /**
34
45
  * Stores Agent history in the shared MySQL immutable record family.
35
46
  *
@@ -38,14 +49,18 @@ export const AgentHistoryHash = {
38
49
  export class MysqlAgentHistory {
39
50
  input;
40
51
  records;
52
+ onAdmit;
53
+ transaction;
54
+ /**
55
+ * Maximum immutable Agent history record bytes supported by MySQL.
56
+ */
57
+ capacity = Object.freeze({ historyRecordBytes: 65_535 });
41
58
  /**
42
59
  * Records whether this handle accepts calls.
43
60
  */
44
61
  #open = true;
45
62
  /**
46
-
47
63
  * Reuses one table and index preparation promise.
48
-
49
64
  */
50
65
  #ready;
51
66
  /**
@@ -53,10 +68,52 @@ export class MysqlAgentHistory {
53
68
  *
54
69
  * @param input Repository and complete tenant scope.
55
70
  * @param records Native record-family storage.
71
+ * @param onAdmit Admits complete public history operations.
72
+ * @param transaction Coordinates native immutable audit groups.
56
73
  */
57
- constructor(input, records) {
74
+ constructor(input, records, onAdmit = (work) => work(), transaction) {
58
75
  this.input = input;
59
76
  this.records = records;
77
+ this.onAdmit = onAdmit;
78
+ this.transaction = transaction;
79
+ }
80
+ /**
81
+ * Writes atomically writes captured immutable rows in the selected tenant database.
82
+ * @param entityId Typed Agent identifier.
83
+ * @param entries Complete Agent audit group.
84
+ * @returns Completion after the native transaction commits.
85
+ */
86
+ appendAll(entityId, entries) {
87
+ return this.onAdmit(() => this.appendAllAdmitted(entityId, entries));
88
+ }
89
+ /**
90
+ * Performs one admitted Agent audit group on a native connection.
91
+ *
92
+ * @param entityId Typed Entity identifier.
93
+ * @param entries Complete immutable history group.
94
+ * @returns Completion after the complete immutable audit group commits atomically.
95
+ */
96
+ async appendAllAdmitted(entityId, entries) {
97
+ this.requireOpen();
98
+ const key = this.key(entityId);
99
+ const rows = entries.map((entry) => {
100
+ if (AgentHistoryKeys.indexValue(AgentHistoryKeys.fromEntry(entry)).length >
101
+ AgentHistoryIndexBytes)
102
+ throw new RangeError("Agent history ordering key exceeds the MySQL index capacity.");
103
+ const row = clone(AgentHistoryRecordSchema, AgentHistoryRecords.record(this.input.stateType, key, entry));
104
+ if (toBinary(AgentHistoryRecordSchema, row).length > 65_535)
105
+ throw new RangeError("Agent history record exceeds the MySQL row capacity.");
106
+ return row;
107
+ });
108
+ if (rows.length === 0)
109
+ return;
110
+ if (this.transaction === undefined)
111
+ throw new Error("MySQL Agent history transaction is unavailable.");
112
+ await this.ready();
113
+ await this.transaction.commit([this.records.tableName], (connection) => this.records.withConnection(connection, async () => {
114
+ for (const row of rows)
115
+ await this.records.writeImmutable(row);
116
+ }));
60
117
  }
61
118
  /**
62
119
  * Writes one original entry or confirms byte-identical content.
@@ -66,6 +123,16 @@ export class MysqlAgentHistory {
66
123
  * @returns Completion after immutable storage confirms the entry.
67
124
  */
68
125
  async append(entityId, entry) {
126
+ return this.onAdmit(() => this.appendAdmitted(entityId, entry));
127
+ }
128
+ /**
129
+ * Executes an admitted provider operation.
130
+ *
131
+ * @param entityId Typed Agent identifier.
132
+ * @param entry Original entry.
133
+ * @returns Completion after immutable storage.
134
+ */
135
+ async appendAdmitted(entityId, entry) {
69
136
  this.requireOpen();
70
137
  if (AgentHistoryKeys.indexValue(AgentHistoryKeys.fromEntry(entry)).length > AgentHistoryIndexBytes)
71
138
  throw new RangeError("Agent history ordering key exceeds the MySQL index capacity.");
@@ -79,6 +146,15 @@ export class MysqlAgentHistory {
79
146
  * @returns Original entries and continuation status.
80
147
  */
81
148
  async read(request) {
149
+ return this.onAdmit(() => this.readAdmitted(request));
150
+ }
151
+ /**
152
+ * Executes an admitted provider operation.
153
+ *
154
+ * @param request Typed history page request.
155
+ * @returns Original entries and continuation status.
156
+ */
157
+ async readAdmitted(request) {
82
158
  this.requireOpen();
83
159
  await this.ready();
84
160
  const agentKey = this.key(request.entityId);
@@ -87,9 +163,7 @@ export class MysqlAgentHistory {
87
163
  return AgentHistoryPages.read(request, after, (cursor, limit) => this.fetch(agentKey, request, cursor, limit));
88
164
  }
89
165
  /**
90
-
91
166
  * Closes this handle without deleting stored records.
92
-
93
167
  */
94
168
  close() {
95
169
  if (!this.#open)
@@ -103,7 +177,10 @@ export class MysqlAgentHistory {
103
177
  * @returns Shared preparation promise for this handle.
104
178
  */
105
179
  ready() {
106
- return (this.#ready ??= this.initialize());
180
+ return (this.#ready ??= this.initialize().catch((error) => {
181
+ this.#ready = undefined;
182
+ throw error;
183
+ }));
107
184
  }
108
185
  /**
109
186
  * Creates each native index idempotently on the shared record family.
@@ -112,17 +189,8 @@ export class MysqlAgentHistory {
112
189
  */
113
190
  async initialize() {
114
191
  await this.records.prepare();
115
- await this.records.ensureHistoryIndex("agent_history_full", ["scope_digest", "order_key"]);
116
- await this.records.ensureHistoryIndex("agent_history_category", [
117
- "scope_digest",
118
- "category",
119
- "order_key",
120
- ]);
121
- await this.records.ensureHistoryIndex("agent_history_conversation", [
122
- "scope_digest",
123
- "conversation_digest",
124
- "order_key",
125
- ]);
192
+ for (const [name, columns] of AgentHistoryIndexes)
193
+ await this.records.ensureHistoryIndex(name, columns);
126
194
  }
127
195
  /**
128
196
  * Reads a bounded native page with complete scope and view predicates.
@@ -200,9 +268,7 @@ export class MysqlAgentHistory {
200
268
  return key;
201
269
  }
202
270
  /**
203
-
204
271
  * Rejects calls after this handle closes.
205
-
206
272
  */
207
273
  requireOpen() {
208
274
  if (!this.#open)