@prestack/thread 0.0.1 → 0.0.3

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 (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -0,0 +1,470 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ import type { Actor } from '../core/actor.ts'
4
+ import type {
5
+ Store, Row, Seams, Telemetry, Log, Audit, Errors, Search, Cost, Idempotency, EventBus, RateLimiter, PlatformEvent, LogLevel,
6
+ Queue, Job, EnqueueOpts, Transactional, FeatureGate, Quotas, Outbox, AuditRecord, ErrorRecord, Vault,
7
+ Files, Views, FieldList } from './seams.ts'
8
+ import { backoffMs, isExhausted } from './queue-policy.ts'
9
+
10
+ function projectRow(row: Row, fields: FieldList, key: string): Row {
11
+ const keep = new Set<string>([key, 'version', ...fields])
12
+ const projected: Row = {}
13
+ for (const [name, value] of Object.entries(row)) {
14
+ if (keep.has(name)) projected[name] = value
15
+ }
16
+ return projected
17
+ }
18
+
19
+ export class UnknownColumnError extends Error {
20
+ constructor(entity: string, key: string) {
21
+ super(
22
+ `${entity} does not declare "${key}", so writing it would be silently discarded. ` +
23
+ 'Declare the field or relation on the entity, or stop writing the key.',
24
+ )
25
+ this.name = 'UnknownColumnError'
26
+ }
27
+ }
28
+
29
+ export class VersionConflictError extends Error {
30
+ constructor(entity: string, id: string, expectedVersion: number) {
31
+ super(
32
+ `${entity} ${id} was not at version ${expectedVersion} — someone else changed it first. ` +
33
+ 're-read the row and retry with its current ETag.',
34
+ )
35
+ this.name = 'VersionConflictError'
36
+ }
37
+ }
38
+
39
+ export class MemoryStore implements Store, Transactional {
40
+ private data = new Map<string, Row[]>()
41
+
42
+ private readonly declared: ReadonlyMap<string, ReadonlySet<string>>
43
+
44
+ private readonly keys: ReadonlyMap<string, string>
45
+
46
+ constructor(declared?: ReadonlyMap<string, ReadonlySet<string>>, keys?: ReadonlyMap<string, string>) {
47
+ this.declared = declared ?? new Map()
48
+ this.keys = keys ?? new Map()
49
+ }
50
+
51
+ private keyOf(entity: string): string {
52
+ const held = this.keys.get(entity)
53
+ if (held === undefined) {
54
+ throw new Error(
55
+ `nothing told this store which value identifies a ${entity}, so it cannot find one row rather than all of ` +
56
+ 'them. An entity states that with a `unique` invariant over one of its values, and whatever builds this ' +
57
+ 'store passes those along with the columns each entity declares.',
58
+ )
59
+ }
60
+ return held
61
+ }
62
+
63
+ private static readonly SYSTEM = new Set(['version'])
64
+
65
+ private assertDeclared(entity: string, row: Record<string, unknown>): void {
66
+ const allowed = this.declared.get(entity)
67
+ if (allowed === undefined) return
68
+ for (const key of Object.keys(row)) {
69
+ if (allowed.has(key) || MemoryStore.SYSTEM.has(key)) continue
70
+ throw new UnknownColumnError(entity, key)
71
+ }
72
+ }
73
+
74
+ private depth = 0
75
+
76
+ async transaction<T>(_actor: Actor, fn: () => Promise<T>): Promise<T> {
77
+ if (this.depth > 0) return fn()
78
+ const snapshot = new Map<string, Row[]>()
79
+ for (const [key, rows] of this.data) snapshot.set(key, rows.map((r) => ({ ...r })))
80
+
81
+ this.depth += 1
82
+ try {
83
+ return await fn()
84
+ } catch (err) {
85
+ this.data = snapshot
86
+ throw err
87
+ } finally {
88
+ this.depth -= 1
89
+ }
90
+ }
91
+
92
+ async insert(_actor: Actor, entity: string, row: Row): Promise<Row> {
93
+ this.assertDeclared(entity, row)
94
+ const rows = this.data.get(entity) ?? []
95
+
96
+ const rec = { ...row, version: 1 }
97
+ rows.push(rec)
98
+ this.data.set(entity, rows)
99
+ return { ...rec }
100
+ }
101
+
102
+ async get(_actor: Actor, entity: string, key: string | number, fields: FieldList): Promise<Row | null> {
103
+ const named = this.keyOf(entity)
104
+ const row = this.data.get(entity)?.find((r) => r[named] === key) ?? null
105
+ return row === null ? null : projectRow(row, fields, named)
106
+ }
107
+ async list(
108
+ _actor: Actor,
109
+ entity: string,
110
+ opts?: { limit?: number; where?: Readonly<Record<string, string | number | boolean | null>> },
111
+ ): Promise<Row[]> {
112
+ const all = this.data.get(entity) ?? []
113
+
114
+ const where = Object.entries(opts?.where ?? {})
115
+ for (const [field] of where) this.assertDeclared(entity, { [field]: null })
116
+
117
+ const matched = where.length === 0 ? all : all.filter((row) => where.every(([f, v]) => (row[f] ?? null) === v))
118
+ return opts?.limit ? matched.slice(0, opts.limit).map((r) => ({ ...r })) : matched.map((r) => ({ ...r }))
119
+ }
120
+ async update(
121
+ _actor: Actor,
122
+ entity: string,
123
+ key: string | number,
124
+ patch: Record<string, unknown>,
125
+ expectedVersion?: number,
126
+ ): Promise<Row> {
127
+ this.assertDeclared(entity, patch)
128
+ const named = this.keyOf(entity)
129
+ const rows = this.data.get(entity) ?? []
130
+ const i = rows.findIndex((r) => r[named] === key)
131
+ const cur = rows[i]
132
+ if (i < 0 || !cur) throw new Error(`record ${String(key)} not found`)
133
+ const currentVersion = typeof cur['version'] === 'number' ? cur['version'] : 1
134
+ if (expectedVersion !== undefined && expectedVersion !== currentVersion) {
135
+ throw new VersionConflictError(entity, String(key), expectedVersion)
136
+ }
137
+
138
+ const next = { ...cur, ...patch, [named]: key, version: currentVersion + 1 }
139
+ rows[i] = next
140
+ return { ...next }
141
+ }
142
+ async remove(_actor: Actor, entity: string, key: string | number): Promise<void> {
143
+ const named = this.keyOf(entity)
144
+ this.data.set(entity, (this.data.get(entity) ?? []).filter((r) => r[named] !== key))
145
+ }
146
+ }
147
+
148
+ export class MemoryQueue implements Queue {
149
+ private jobs = new Map<string, Job & { runAt: number; claimed: boolean; dedupeKey?: string }>()
150
+ private dead: Job[] = []
151
+ private seq = 0
152
+
153
+ async enqueue<T>(actor: Actor, kind: string, payload: T, opts?: EnqueueOpts): Promise<string> {
154
+
155
+ if (opts?.dedupeKey) {
156
+ for (const [id, job] of this.jobs) {
157
+ if (job.dedupeKey === opts.dedupeKey) return id
158
+ }
159
+ }
160
+ const id = `job_${++this.seq}`
161
+ this.jobs.set(id, {
162
+ id,
163
+ kind,
164
+ payload,
165
+ attempts: 0,
166
+ runAt: opts?.runAt ?? 0,
167
+ claimed: false,
168
+ dedupeKey: opts?.dedupeKey,
169
+ actor,
170
+ })
171
+ return id
172
+ }
173
+
174
+ async poll(kinds: string[], limit: number, now: number): Promise<Job[]> {
175
+ const due: Job[] = []
176
+ for (const job of this.jobs.values()) {
177
+ if (due.length >= limit) break
178
+ if (job.claimed || !kinds.includes(job.kind) || job.runAt > now) continue
179
+ job.claimed = true
180
+ due.push({ ...job })
181
+ }
182
+ return due
183
+ }
184
+
185
+ async ack(id: string): Promise<void> {
186
+ this.jobs.delete(id)
187
+ }
188
+
189
+ async fail(id: string, error: string, now: number): Promise<void> {
190
+ const job = this.jobs.get(id)
191
+ if (!job) return
192
+ job.attempts += 1
193
+ job.claimed = false
194
+
195
+ if (isExhausted(job.attempts)) {
196
+
197
+ this.dead.push({ ...job })
198
+ this.jobs.delete(id)
199
+ return
200
+ }
201
+ job.runAt = now + backoffMs(job.attempts)
202
+ void error
203
+ }
204
+
205
+ async deadLetter(id: string, error: string): Promise<void> {
206
+ const job = this.jobs.get(id)
207
+ if (!job) return
208
+ this.dead.push({ ...job })
209
+ this.jobs.delete(id)
210
+ void error
211
+ }
212
+
213
+ async deadLetters(): Promise<Job[]> {
214
+ return this.dead.map((j) => ({ ...j }))
215
+ }
216
+ }
217
+
218
+ export interface Sink {
219
+ logs: { level: LogLevel; action: string; data: Record<string, unknown> }[]
220
+ events: PlatformEvent[]
221
+ audits: AuditRecord[]
222
+ errors: ErrorRecord[]
223
+ costs: string[]
224
+ indexed: { entity: string; row: Row }[]
225
+ spans: { action: string; status: 'ok' | 'error' }[]
226
+
227
+ outbox: PlatformEvent[]
228
+
229
+ gateOrder: string[]
230
+
231
+ quota: { reserved: number[]; committed: number[]; released: number }
232
+
233
+ files: { action: 'put' | 'get' | 'signed' | 'remove'; key: string; bytes: number | null }[]
234
+
235
+ queries: { sql: string; params: readonly unknown[]; rows: number }[]
236
+ }
237
+
238
+ export class MemoryIdempotency implements Idempotency {
239
+ private readonly records: Map<string, unknown>
240
+ private readonly onCheck?: () => void
241
+
242
+ constructor(opts: {
243
+ readonly onCheck?: () => void
244
+
245
+ readonly records?: Map<string, unknown>
246
+ }) {
247
+ this.records = opts.records ?? new Map<string, unknown>()
248
+ this.onCheck = opts.onCheck
249
+ }
250
+
251
+ private keyFor(key: string, action: string): string {
252
+ return `${key}:${action}`
253
+ }
254
+
255
+ async check(key: string | undefined, action: string): Promise<unknown | null> {
256
+ this.onCheck?.()
257
+ if (!key) return null
258
+ const k = this.keyFor(key, action)
259
+ return this.records.has(k) ? (this.records.get(k) ?? null) : null
260
+ }
261
+
262
+ async store(key: string | undefined, action: string, output: unknown): Promise<void> {
263
+ if (!key) return
264
+ const k = this.keyFor(key, action)
265
+
266
+ if (!this.records.has(k)) this.records.set(k, output)
267
+ }
268
+ }
269
+
270
+ export interface MemorySeamOptions {
271
+ readonly allowRate?: boolean
272
+
273
+ readonly features?: readonly string[]
274
+
275
+ readonly quota?: Readonly<Record<string, number>>
276
+ readonly onExceed?: string
277
+
278
+ readonly entities?: ReadonlyMap<string, ReadonlySet<string>>
279
+
280
+ readonly keys?: ReadonlyMap<string, string>
281
+
282
+ readonly secrets?: Readonly<Record<string, string>>
283
+
284
+ readonly views?: Readonly<Record<string, readonly unknown[]>>
285
+ }
286
+
287
+ export function memoryVault(seed: Readonly<Record<string, string>> = {}): Vault {
288
+ return {
289
+ async resolve(refs) {
290
+ const found: Record<string, string> = {}
291
+ for (const ref of refs) {
292
+ const value = seed[ref]
293
+ if (value === undefined) throw new Error(`vault has no value for "${ref}"`)
294
+ found[ref] = value
295
+ }
296
+ return found
297
+ },
298
+ }
299
+ }
300
+
301
+ export const arrived = (): number => Date.now()
302
+
303
+ export const fresh = (): string => randomUUID()
304
+
305
+ export function inMemorySeams(opts?: MemorySeamOptions): {
306
+ seams: Seams
307
+ sink: Sink
308
+ store: MemoryStore
309
+ queue: MemoryQueue
310
+ } {
311
+ const sink: Sink = {
312
+ logs: [], events: [], audits: [], errors: [], costs: [], indexed: [], spans: [],
313
+ outbox: [], gateOrder: [], quota: { reserved: [], committed: [], released: 0 },
314
+ files: [], queries: [],
315
+ }
316
+ const store = new MemoryStore(opts?.entities, opts?.keys)
317
+ const queue = new MemoryQueue()
318
+ const searchIdx = new Map<string, Row[]>()
319
+
320
+ const telemetry: Telemetry = {
321
+ start(action) {
322
+ let status: 'ok' | 'error' = 'ok'
323
+ return {
324
+ ok() { status = 'ok' },
325
+ error() { status = 'error' },
326
+ end() { sink.spans.push({ action, status }) },
327
+ }
328
+ },
329
+ }
330
+ const log: Log = { write(level, action, _actor, data) { sink.logs.push({ level, action, data }) } }
331
+
332
+ const audit: Audit = { async append(_actor, record) { sink.audits.push(record) } }
333
+ const errors: Errors = { async record(_actor, record) { sink.errors.push(record) } }
334
+ const cost: Cost = { meter(action) { sink.costs.push(action) } }
335
+ const events: EventBus = { async publish(e) { sink.events.push(e) } }
336
+ const rateLimiter: RateLimiter = {
337
+ async check() {
338
+ sink.gateOrder.push('rate')
339
+ return opts?.allowRate ?? true
340
+ },
341
+ async checkPolicy() {
342
+ sink.gateOrder.push('rate')
343
+ return opts?.allowRate ?? true
344
+ },
345
+ }
346
+
347
+ const featureGate: FeatureGate | undefined =
348
+ opts?.features === undefined
349
+ ? undefined
350
+ : {
351
+ async check(_actor, feature) {
352
+ sink.gateOrder.push('feature')
353
+ return (opts.features ?? []).includes(feature)
354
+ },
355
+ }
356
+
357
+ const used = new Map<string, number>()
358
+ const quotas: Quotas | undefined =
359
+ opts?.quota === undefined
360
+ ? undefined
361
+ : {
362
+ async reserve(_actor, meter, estimate) {
363
+ sink.gateOrder.push('quota')
364
+ sink.quota.reserved.push(estimate)
365
+
366
+ const allowance = opts.quota?.[meter] ?? Number.POSITIVE_INFINITY
367
+ const before = used.get(meter) ?? 0
368
+
369
+ if (before + estimate > allowance) {
370
+
371
+ return {
372
+ ok: false,
373
+ behavior: opts.onExceed ?? 'block',
374
+ retryAfterSeconds: 60,
375
+ async commit() {},
376
+ async release() {},
377
+ }
378
+ }
379
+
380
+ used.set(meter, before + estimate)
381
+ let settled = false
382
+ return {
383
+ ok: true,
384
+ behavior: null,
385
+ retryAfterSeconds: 0,
386
+ async commit(actual) {
387
+ if (settled) return
388
+ settled = true
389
+ sink.quota.committed.push(actual)
390
+ used.set(meter, (used.get(meter) ?? 0) - estimate + actual)
391
+ },
392
+ async release() {
393
+ if (settled) return
394
+ settled = true
395
+ sink.quota.released += 1
396
+ used.set(meter, (used.get(meter) ?? 0) - estimate)
397
+ },
398
+ }
399
+ },
400
+ }
401
+
402
+ const outbox: Outbox = {
403
+ async emit(_actor, event) {
404
+ sink.outbox.push(event)
405
+ },
406
+ }
407
+
408
+ const idempotency: Idempotency = new MemoryIdempotency({
409
+ onCheck: () => sink.gateOrder.push('idempotency'),
410
+ })
411
+ const search: Search = {
412
+ async index(_actor, entity, row) {
413
+ sink.indexed.push({ entity, row })
414
+ searchIdx.set(entity, [...(searchIdx.get(entity) ?? []), row])
415
+ },
416
+ async query(_actor, entity, q) {
417
+
418
+ const needle = q.toLowerCase()
419
+ return (searchIdx.get(entity) ?? []).filter((r) => JSON.stringify(r).toLowerCase().includes(needle))
420
+ },
421
+ }
422
+
423
+ const vault: Vault | undefined = opts?.secrets === undefined ? undefined : memoryVault(opts.secrets)
424
+
425
+ const stored = new Map<string, { body: Uint8Array; contentType: string }>()
426
+ const files: Files = {
427
+ async put(_actor, key, body, contentType) {
428
+ stored.set(key, { body, contentType })
429
+ sink.files.push({ action: 'put', key, bytes: body.byteLength })
430
+ return { size: body.byteLength }
431
+ },
432
+ async get(_actor, key) {
433
+ const found = stored.get(key)
434
+ sink.files.push({ action: 'get', key, bytes: found === undefined ? null : found.body.byteLength })
435
+ return found === undefined ? null : { body: found.body, contentType: found.contentType }
436
+ },
437
+ async signedUrl(_actor, key, expiresInSeconds) {
438
+ sink.files.push({ action: 'signed', key, bytes: null })
439
+ return `https://files.invalid/unsigned-in-memory/${encodeURIComponent(key)}?expires_in=${String(expiresInSeconds)}`
440
+ },
441
+ async remove(_actor, key) {
442
+ stored.delete(key)
443
+ sink.files.push({ action: 'remove', key, bytes: null })
444
+ },
445
+ }
446
+
447
+ const answers = opts?.views
448
+ const views: Views | undefined =
449
+ answers === undefined
450
+ ? undefined
451
+ : {
452
+ async query<T>(_actor: Actor, sql: string, params: readonly unknown[]) {
453
+ const found = (answers[sql] ?? []) as readonly T[]
454
+ sink.queries.push({ sql, params, rows: found.length })
455
+ return found
456
+ },
457
+ }
458
+
459
+ return {
460
+ seams: {
461
+ store, telemetry, log, audit, errors, search, cost, idempotency, events, rateLimiter, queue,
462
+ outbox, files, clock: { now: arrived }, random: { next: Math.random, uuid: fresh },
463
+ ...(featureGate ? { featureGate } : {}),
464
+ ...(quotas ? { quotas } : {}),
465
+ ...(vault ? { vault } : {}),
466
+ ...(views ? { views } : {}),
467
+ },
468
+ sink, store, queue,
469
+ }
470
+ }