@kairos-es/store-postgres 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +167 -0
  3. package/dist/cjs/clients.js +62 -0
  4. package/dist/cjs/clients.js.map +1 -0
  5. package/dist/cjs/config.js +94 -0
  6. package/dist/cjs/config.js.map +1 -0
  7. package/dist/cjs/ensureSchema.js +53 -0
  8. package/dist/cjs/ensureSchema.js.map +1 -0
  9. package/dist/cjs/index.js +45 -0
  10. package/dist/cjs/index.js.map +1 -0
  11. package/dist/cjs/internal/ddl.js +436 -0
  12. package/dist/cjs/internal/ddl.js.map +1 -0
  13. package/dist/cjs/internal/matchSql.js +189 -0
  14. package/dist/cjs/internal/matchSql.js.map +1 -0
  15. package/dist/cjs/internal/readPlan.js +67 -0
  16. package/dist/cjs/internal/readPlan.js.map +1 -0
  17. package/dist/cjs/internal/subscribe.js +120 -0
  18. package/dist/cjs/internal/subscribe.js.map +1 -0
  19. package/dist/cjs/internal/transport.js +66 -0
  20. package/dist/cjs/internal/transport.js.map +1 -0
  21. package/dist/cjs/store.js +256 -0
  22. package/dist/cjs/store.js.map +1 -0
  23. package/dist/dts/clients.d.ts +34 -0
  24. package/dist/dts/clients.d.ts.map +1 -0
  25. package/dist/dts/config.d.ts +58 -0
  26. package/dist/dts/config.d.ts.map +1 -0
  27. package/dist/dts/ensureSchema.d.ts +35 -0
  28. package/dist/dts/ensureSchema.d.ts.map +1 -0
  29. package/dist/dts/index.d.ts +41 -0
  30. package/dist/dts/index.d.ts.map +1 -0
  31. package/dist/dts/internal/ddl.d.ts +174 -0
  32. package/dist/dts/internal/ddl.d.ts.map +1 -0
  33. package/dist/dts/internal/matchSql.d.ts +140 -0
  34. package/dist/dts/internal/matchSql.d.ts.map +1 -0
  35. package/dist/dts/internal/readPlan.d.ts +72 -0
  36. package/dist/dts/internal/readPlan.d.ts.map +1 -0
  37. package/dist/dts/internal/subscribe.d.ts +87 -0
  38. package/dist/dts/internal/subscribe.d.ts.map +1 -0
  39. package/dist/dts/internal/transport.d.ts +116 -0
  40. package/dist/dts/internal/transport.d.ts.map +1 -0
  41. package/dist/dts/store.d.ts +17 -0
  42. package/dist/dts/store.d.ts.map +1 -0
  43. package/dist/esm/clients.js +51 -0
  44. package/dist/esm/clients.js.map +1 -0
  45. package/dist/esm/config.js +87 -0
  46. package/dist/esm/config.js.map +1 -0
  47. package/dist/esm/ensureSchema.js +45 -0
  48. package/dist/esm/ensureSchema.js.map +1 -0
  49. package/dist/esm/index.js +40 -0
  50. package/dist/esm/index.js.map +1 -0
  51. package/dist/esm/internal/ddl.js +424 -0
  52. package/dist/esm/internal/ddl.js.map +1 -0
  53. package/dist/esm/internal/matchSql.js +176 -0
  54. package/dist/esm/internal/matchSql.js.map +1 -0
  55. package/dist/esm/internal/readPlan.js +59 -0
  56. package/dist/esm/internal/readPlan.js.map +1 -0
  57. package/dist/esm/internal/subscribe.js +113 -0
  58. package/dist/esm/internal/subscribe.js.map +1 -0
  59. package/dist/esm/internal/transport.js +56 -0
  60. package/dist/esm/internal/transport.js.map +1 -0
  61. package/dist/esm/package.json +4 -0
  62. package/dist/esm/store.js +249 -0
  63. package/dist/esm/store.js.map +1 -0
  64. package/package.json +35 -0
  65. package/src/clients.ts +69 -0
  66. package/src/config.ts +92 -0
  67. package/src/ensureSchema.ts +46 -0
  68. package/src/index.ts +45 -0
  69. package/src/internal/ddl.ts +599 -0
  70. package/src/internal/matchSql.ts +219 -0
  71. package/src/internal/readPlan.ts +117 -0
  72. package/src/internal/transport.ts +141 -0
  73. package/src/store.ts +413 -0
package/src/store.ts ADDED
@@ -0,0 +1,413 @@
1
+ /**
2
+ * The Postgres `DcbEventStore` engine: the runtime `read`/`append`/`subscribe`
3
+ * over the two-table B-tree + exclusive-lock design (ADR-0002). It satisfies the
4
+ * SAME `DcbEventStore` contract as the in-memory oracle, so the shared
5
+ * `store-contract-tests` suite runs unchanged against it with in-memory as the
6
+ * parity reference.
7
+ *
8
+ * Concurrency correctness does NOT live in TypeScript here — it lives in the
9
+ * plpgsql append function's `LOCK TABLE … IN EXCLUSIVE MODE` (see
10
+ * `internal/ddl.ts`). The TS side is transport: shape a `Query`/event batch into
11
+ * JSON, call the function, and map rows back to core types. In particular:
12
+ *
13
+ * - a CONFLICT is a ZERO-ROW function result, read from the row count — NEVER a
14
+ * caught SQL error. `append` maps empty → `AppendConditionFailed`, one row →
15
+ * the `MAX(id)` head position.
16
+ * - `position` is `bigserial`, so it is strictly-increasing but NOT gapless; we
17
+ * order by `id` alone and never assume contiguity.
18
+ * - `subscribe` opens `listen` on `PgClientDirect` BEFORE the catch-up read, then
19
+ * reconciles by paged reads and de-dups the live tail past the caught-up
20
+ * boundary — the one place the two clients diverge (the Neon trap).
21
+ *
22
+ * That zero-row signal cannot be FORGED by a lost response, and what buys it is
23
+ * the absence of a retry rather than a check. `append` issues its guarded
24
+ * statement exactly ONCE: nothing in this engine re-issues one, and
25
+ * `retryOnConflict` resumes only on `AppendConditionFailed`, which an
26
+ * infrastructure fault is not — `makeContractStore` dies it, off the error
27
+ * channel a retry watches. So a write that reaches the server, commits, and
28
+ * loses its response surfaces as a DEFECT over an outcome this engine does not
29
+ * resolve, never as a conflict that never happened; there is no second issue of
30
+ * the guarded statement to meet its own row and read zero. Surfacing rather than
31
+ * RESOLVING is what makes that free: an engine willing to re-issue an undecided
32
+ * append needs a way to tell its own committed row from a rival's BEFORE it does,
33
+ * or the re-issue is the very thing that manufactures the phantom — see
34
+ * `@kairos-es/store-sqlite`, which resolves one by identity and re-issues only
35
+ * when identity answers absent. So the missing verify path here is a consequence
36
+ * of that choice rather than an omission from it.
37
+ *
38
+ * This module is WIRING + APPEND: the JSON/JSONB transport (the event/query-item
39
+ * (de)serialisers and row mapping) lives in `internal/transport.ts` and the
40
+ * read-plan compiler in `internal/readPlan.ts`. The wake/poll/de-dup subscribe
41
+ * machine is NOT here either — it is core's `subscribeStream`
42
+ * (`packages/core/src/SubscribeMachine.ts`), shared by every engine, and this
43
+ * module only hands it a `fetch` and a `listen` built from the two clients.
44
+ */
45
+ import type { SqlClient, SqlError } from '@effect/sql'
46
+ import type {
47
+ DcbEvent,
48
+ Position,
49
+ ReadOptions,
50
+ ReadResponse,
51
+ SequencedEvent,
52
+ } from '@kairos-es/core'
53
+ import {
54
+ type AppendCondition,
55
+ AppendConditionFailed,
56
+ DcbEventStore,
57
+ DEFAULT_CATCH_UP_PAGE_SIZE,
58
+ DEFAULT_POLL_INTERVAL_MILLIS,
59
+ makeContractStore,
60
+ ORIGIN,
61
+ type Query,
62
+ type RawDcbEventStore,
63
+ subscribeStream,
64
+ } from '@kairos-es/core'
65
+ import { type Array as Arr, Effect, Layer, type Scope, Stream } from 'effect'
66
+ import { PgClientDirect, PgClientPooled } from './clients'
67
+ import { decodePostgresStoreConfig, type PostgresStoreConfig } from './config'
68
+ import {
69
+ quoteQualified,
70
+ type ResolvedNames,
71
+ resolveNames,
72
+ } from './internal/ddl'
73
+ import {
74
+ compileFetchPlan,
75
+ type ReadTables,
76
+ runFetch,
77
+ } from './internal/readPlan'
78
+ import {
79
+ type EventRow,
80
+ jsonbText,
81
+ rowToSequenced,
82
+ toEventJson,
83
+ toQueryItemsJson,
84
+ } from './internal/transport'
85
+
86
+ /**
87
+ * Set `lock_timeout` for the current transaction via a BOUND parameter.
88
+ *
89
+ * `SET LOCAL` cannot take a bound parameter, but `set_config(setting, value,
90
+ * is_local)` is its exact bound-parameter equivalent (`is_local = true` ≡ `SET
91
+ * LOCAL`) — so the timeout value is a parameter, never interpolated into SQL
92
+ * text. The value is `'<seconds>s'` (Postgres parses the `s` unit); `'0s'` = no
93
+ * timeout. Runs inside the append transaction so it scopes to that call only.
94
+ */
95
+ const setLocalLockTimeout = (
96
+ sql: SqlClient.SqlClient,
97
+ lockTimeout: number,
98
+ ): Effect.Effect<unknown, SqlError.SqlError> =>
99
+ sql`SELECT set_config('lock_timeout', ${`${lockTimeout}s`}, true)`
100
+
101
+ /**
102
+ * Build the engine MECHANICS (a `RawDcbEventStore`) over the two clients and a
103
+ * resolved name set. Contract policy (`assertServableQuery`, `ReadLimit` decode,
104
+ * error-vs-defect classification) is NOT here — `makeContractStore` adds it
105
+ * uniformly, so this engine surfaces raw `SqlError`s and the contract conflict.
106
+ */
107
+ const make = (
108
+ config: PostgresStoreConfig,
109
+ names: ResolvedNames,
110
+ ): Effect.Effect<
111
+ RawDcbEventStore<SqlError.SqlError>,
112
+ never,
113
+ PgClientPooled | PgClientDirect
114
+ > =>
115
+ Effect.gen(function* () {
116
+ const pooled = yield* PgClientPooled
117
+ const direct = yield* PgClientDirect
118
+ const lockTimeout = config.lockTimeout ?? 0
119
+ const pollInterval = config.pollInterval ?? DEFAULT_POLL_INTERVAL_MILLIS
120
+ const catchUpPageSize = config.catchUpPageSize ?? DEFAULT_CATCH_UP_PAGE_SIZE
121
+
122
+ // The pre-quoted table names every read plan embeds (resolved once).
123
+ const tables: ReadTables = {
124
+ main: quoteQualified(names.mainTable),
125
+ tag: quoteQualified(names.tagTable),
126
+ }
127
+
128
+ // --- read -------------------------------------------------------------
129
+
130
+ /**
131
+ * The global last position ignoring any filter — the head of a no-limit
132
+ * read. Empty store → ORIGIN.
133
+ *
134
+ * A VALUE over the resolved `pooled`, and deliberately NOT a function of a
135
+ * client — do not re-introduce the parameter. There is only one client it
136
+ * could take: `direct` exists solely for `subscribe`'s `listen` (see
137
+ * `clients.ts`), so a client parameter would advertise a selection seam the
138
+ * engine does not have.
139
+ *
140
+ * Nor would such a parameter be load-bearing for the REPEATABLE READ
141
+ * transaction this runs inside, which is the trap worth naming, since a
142
+ * closed-over client LOOKS like it must escape to its own snapshot.
143
+ * `@effect/sql` resolves a statement's connection when the statement RUNS,
144
+ * from the FIBRE CONTEXT rather than from the client value: `withTransaction`
145
+ * acquires one connection and `Effect.locally`s it into
146
+ * `FiberRef.currentContext` under its `TransactionConnection` tag, and every
147
+ * statement a client builds resolves through a `getConnection` that reads
148
+ * that tag first, falling back to the pool acquirer only when it is absent
149
+ * (`@effect/sql@0.52` `src/internal/client.ts`). A statement therefore joins
150
+ * a transaction by being RUN inside it, and passing the client along the call
151
+ * chain neither adds to that nor is required by it. `read-postgres`'s
152
+ * `pinnedConnection` test observes the same fact from the SERVER: two
153
+ * statements issued inside one transaction report one `pg_backend_pid()`.
154
+ *
155
+ * Built once at layer construction and re-run per read, which a statement
156
+ * supports: it carries no mutable state and recompiles on every execution.
157
+ */
158
+ const globalHead: Effect.Effect<Position, SqlError.SqlError> = pooled<{
159
+ readonly max: string | null
160
+ }>`
161
+ SELECT MAX(id) AS max FROM ${pooled(names.mainTable)}
162
+ `.pipe(
163
+ Effect.map((rows) => {
164
+ const max = rows[0]?.max
165
+ return max === null || max === undefined
166
+ ? ORIGIN
167
+ : (BigInt(max) as Position)
168
+ }),
169
+ )
170
+
171
+ /**
172
+ * The read MECHANICS: the rows a query selects, plus the head a decision
173
+ * model may then append under. `read` below only shapes the pair into a
174
+ * `ReadResponse`, so the two snapshot regimes argued for here stay in one
175
+ * place.
176
+ *
177
+ * Named for what it returns rather than for what it reads through: it closes
178
+ * over `pooled`, and takes no client, for the reason given on `globalHead`.
179
+ */
180
+ const readRowsAndHead = (
181
+ query: Query,
182
+ options?: ReadOptions,
183
+ ): Effect.Effect<
184
+ { readonly rows: ReadonlyArray<EventRow>; readonly head: Position },
185
+ SqlError.SqlError
186
+ > =>
187
+ Effect.gen(function* () {
188
+ // No validation here — the query and limit were validated once at the
189
+ // `makeContractStore` boundary, so the per-read re-validation is gone.
190
+ const plan = compileFetchPlan(tables, query)
191
+
192
+ // Limited read: `head` is the last RETURNED position, taken from the
193
+ // SAME single statement, so rows and head are already one consistent
194
+ // snapshot (or ORIGIN when nothing is returned). No transaction needed —
195
+ // a single statement sees a single snapshot.
196
+ if (options?.limit !== undefined) {
197
+ const rows = yield* runFetch(
198
+ pooled,
199
+ plan,
200
+ options.after,
201
+ options.limit,
202
+ )
203
+ const lastRow = rows.at(-1)
204
+ const head =
205
+ lastRow === undefined ? ORIGIN : (BigInt(lastRow.id) as Position)
206
+ return { rows, head }
207
+ }
208
+
209
+ // No-limit read: `head` is the GLOBAL last position (ignoring the query
210
+ // filter), which needs a SEPARATE `MAX(id)` statement. Rows and head
211
+ // MUST share one MVCC snapshot: under the default READ COMMITTED each
212
+ // statement snapshots independently, so a concurrent append committing
213
+ // between the row fetch and the `MAX(id)` would push `head` PAST an
214
+ // event absent from `events` — and a decision model appending under
215
+ // `{ after: head }` would then silently miss that event as a conflict,
216
+ // breaking optimistic concurrency. A REPEATABLE READ, READ ONLY
217
+ // transaction gives both statements a single snapshot, matching the
218
+ // in-memory oracle's atomic (mutex-guarded) read. Readers take only
219
+ // ACCESS SHARE, so this never contends with the exclusive-lock append.
220
+ return yield* pooled.withTransaction(
221
+ Effect.gen(function* () {
222
+ yield* pooled.unsafe(
223
+ 'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY',
224
+ )
225
+ const rows = yield* runFetch(
226
+ pooled,
227
+ plan,
228
+ options?.after,
229
+ undefined,
230
+ )
231
+ const head = yield* globalHead
232
+ return { rows, head }
233
+ }),
234
+ )
235
+ })
236
+
237
+ const read = (
238
+ query: Query,
239
+ options?: ReadOptions,
240
+ ): Effect.Effect<ReadResponse, SqlError.SqlError> =>
241
+ readRowsAndHead(query, options).pipe(
242
+ Effect.map(
243
+ ({ rows, head }): ReadResponse => ({
244
+ events: Stream.fromIterable(rows.map(rowToSequenced)),
245
+ head,
246
+ }),
247
+ ),
248
+ )
249
+
250
+ // --- append -----------------------------------------------------------
251
+
252
+ /**
253
+ * One row of an append function's result: the new head (`MAX(id)`), or NULL /
254
+ * zero rows for the empty/conflict cases the callers interpret.
255
+ */
256
+ type AppendHeadRow = { readonly max: string | null }
257
+
258
+ /**
259
+ * The ONE transaction frame every append rides: open a transaction, apply the
260
+ * per-call `set_config('lock_timeout', …, true)`, then run the caller's
261
+ * `SELECT <fn>(…)`. Both the conditional and unconditional paths go through
262
+ * here, so the lock-timeout frame can never diverge between them — the
263
+ * function's LOCK/check/insert and the timeout are one atomic unit, and the
264
+ * conflict (zero rows) vs head (`MAX(id)`) is read from the returned rows by
265
+ * the caller, never from a caught error.
266
+ */
267
+ const callAppendFn = (
268
+ select: Effect.Effect<ReadonlyArray<AppendHeadRow>, SqlError.SqlError>,
269
+ ): Effect.Effect<ReadonlyArray<AppendHeadRow>, SqlError.SqlError> =>
270
+ pooled.withTransaction(
271
+ setLocalLockTimeout(pooled, lockTimeout).pipe(Effect.zipRight(select)),
272
+ )
273
+
274
+ const append = (
275
+ events: Arr.NonEmptyReadonlyArray<DcbEvent>,
276
+ condition?: AppendCondition,
277
+ ): Effect.Effect<Position, AppendConditionFailed | SqlError.SqlError> => {
278
+ const eventsJson = events.map(toEventJson)
279
+
280
+ // Raw mechanics: a CONFLICT is `AppendConditionFailed`; a `SqlError` (incl.
281
+ // a `55P03` lock-timeout abort) rides the channel too. `makeContractStore`
282
+ // classifies — conflict stays, everything else becomes a defect.
283
+ return Effect.gen(function* () {
284
+ if (condition === undefined) {
285
+ // No guard, but the unconditional twin STILL takes the exclusive lock
286
+ // (see `unconditionalAppendBody`). That lock on BOTH append paths is how
287
+ // this engine discharges the subscribe machine's ascending-commit-order
288
+ // precondition (`SubscribeMachine.ts` argues it): without it two
289
+ // concurrent unconditional writers can commit bigserial ids out of
290
+ // order, letting `subscribe` skip the later-committing lower id and
291
+ // `head` overtake an in-flight position. Sharing `callAppendFn` with the
292
+ // conditional path guarantees the lock is scoped to the call and the
293
+ // configured timeout applies identically to both append paths.
294
+ const rows = yield* callAppendFn(
295
+ pooled<AppendHeadRow>`
296
+ SELECT ${pooled(names.appendUnconditionalFn)}(
297
+ ${jsonbText(eventsJson)}::jsonb
298
+ ) AS max
299
+ `,
300
+ )
301
+ return headFromRows(rows)
302
+ }
303
+
304
+ const queryItems = toQueryItemsJson(condition.failIfEventsMatch)
305
+ const afterId = condition.after === undefined ? null : condition.after
306
+
307
+ // The function returns MAX(id) on success, ZERO rows on conflict — the
308
+ // conflict is the row count, never a caught error.
309
+ const rows = yield* callAppendFn(
310
+ pooled<AppendHeadRow>`
311
+ SELECT ${pooled(names.appendFn)}(
312
+ ${jsonbText(queryItems)}::jsonb,
313
+ ${afterId},
314
+ ${jsonbText(eventsJson)}::jsonb
315
+ ) AS max
316
+ `,
317
+ )
318
+ // Empty result set = conflict.
319
+ if (rows.length === 0) {
320
+ return yield* new AppendConditionFailed({ condition })
321
+ }
322
+ return headFromRows(rows)
323
+ })
324
+ }
325
+
326
+ /**
327
+ * Extract the new head `Position` from the function's single result row. The
328
+ * function returns exactly one row (`MAX(id)`) on a successful insert; that
329
+ * value is the last new position (read-your-writes).
330
+ */
331
+ const headFromRows = (rows: ReadonlyArray<AppendHeadRow>): Position => {
332
+ const max = rows[0]?.max
333
+ if (max === null || max === undefined) {
334
+ // Defensive: a successful unconditional/`NOT conflict` insert always
335
+ // yields a non-null MAX. A null here means nothing was inserted, which
336
+ // for an append (always ≥1 event) is a broken invariant → die.
337
+ throw new Error(
338
+ 'kairos-es/store-postgres: append returned no head position',
339
+ )
340
+ }
341
+ return BigInt(max) as Position
342
+ }
343
+
344
+ // --- subscribe --------------------------------------------------------
345
+
346
+ /**
347
+ * Wire core's subscribe machine to the live clients: `fetch` is the
348
+ * per-subscription compiled read plan run against the pooled client and mapped
349
+ * to `SequencedEvent`s; `listen` is the direct client's ref-counted, non-pooled
350
+ * LISTEN on this store's channel (the Neon-safe path — `clients.ts` owns why
351
+ * there are two clients at all). The plan is compiled ONCE here so the poll
352
+ * loop re-binds only the cursor.
353
+ *
354
+ * The wake source is `NOTIFY`, and it is a LATENCY optimisation only: the
355
+ * machine's periodic position-based poll is what makes delivery correct, which
356
+ * matters more here than the general argument for it suggests. The
357
+ * `@effect/sql-pg` listen client SWALLOWS connection errors and does not
358
+ * auto-reconnect, so a dropped LISTEN typically goes quiet rather than
359
+ * erroring — the machine's listen-side retry then never fires, and the poll is
360
+ * the ONLY thing that recovers the subscription. The same silence is what a
361
+ * Neon pooled endpoint produces if `PgClientDirect` is misconfigured: the
362
+ * pooler drops `LISTEN`/`NOTIFY` without complaint and delivery degrades to
363
+ * poll-rate rather than failing, which is why that trap is a deployment-time
364
+ * wiring choice and not a runtime error.
365
+ *
366
+ * `E` is `SqlError.SqlError` — both seams fail with it, so the machine's single
367
+ * error parameter carries it straight through to `makeContractStore`.
368
+ */
369
+ const subscribe = (
370
+ query: Query,
371
+ after?: Position,
372
+ ): Stream.Stream<SequencedEvent, SqlError.SqlError, Scope.Scope> => {
373
+ const plan = compileFetchPlan(tables, query)
374
+ return subscribeStream<SqlError.SqlError>(
375
+ {
376
+ fetch: (from, limit) =>
377
+ runFetch(pooled, plan, from, limit).pipe(
378
+ Effect.map((rows) => rows.map(rowToSequenced)),
379
+ ),
380
+ listen: direct.listen(names.channel),
381
+ pollInterval,
382
+ catchUpPageSize,
383
+ },
384
+ after,
385
+ )
386
+ }
387
+
388
+ return { read, append, subscribe } as const
389
+ })
390
+
391
+ /**
392
+ * The Postgres `DcbEventStore` layer factory. Builds the engine over
393
+ * `PgClientPooled` (reads/append/notify) and `PgClientDirect` (the subscribe
394
+ * listener). The caller wires those two client layers — pointing `PgClientDirect`
395
+ * at a direct (non-pooler) endpoint on Neon, or at the same URL as `PgClientPooled`
396
+ * everywhere else.
397
+ *
398
+ * `ensureSchema(config)` must have been run (by the caller's migrator or at
399
+ * boot) before the layer is used; the layer itself performs no DDL, so it stays
400
+ * a pure runtime dependency requiring only the two clients.
401
+ */
402
+ export const layer = (
403
+ config: PostgresStoreConfig = {},
404
+ ): Layer.Layer<DcbEventStore, never, PgClientPooled | PgClientDirect> => {
405
+ // Decode through the config schema so a malformed config (empty/dotted atom,
406
+ // negative/non-finite timeout, over-63-byte derived name) fails LOUDLY at
407
+ // construction rather than late on the first append.
408
+ const decoded = decodePostgresStoreConfig(config)
409
+ return Layer.effect(
410
+ DcbEventStore,
411
+ Effect.map(make(decoded, resolveNames(decoded)), makeContractStore),
412
+ )
413
+ }