replyonce 0.0.0-stage → 0.1.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.
- package/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +327 -2
- package/dist/adapters.cjs +260 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.cts +134 -0
- package/dist/adapters.d.ts +134 -0
- package/dist/adapters.js +224 -0
- package/dist/adapters.js.map +1 -0
- package/dist/cli.js +999 -0
- package/dist/index.cjs +839 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +801 -0
- package/dist/index.js.map +1 -0
- package/dist/postgres.cjs +146 -0
- package/dist/postgres.cjs.map +1 -0
- package/dist/postgres.d.cts +48 -0
- package/dist/postgres.d.ts +48 -0
- package/dist/postgres.js +120 -0
- package/dist/postgres.js.map +1 -0
- package/dist/redis.cjs +85 -0
- package/dist/redis.cjs.map +1 -0
- package/dist/redis.d.cts +40 -0
- package/dist/redis.d.ts +40 -0
- package/dist/redis.js +59 -0
- package/dist/redis.js.map +1 -0
- package/dist/replyonce-CGwSjxB2.d.cts +209 -0
- package/dist/replyonce-DNRS4FWY.d.ts +209 -0
- package/dist/types-Dn7TSEnu.d.cts +209 -0
- package/dist/types-Dn7TSEnu.d.ts +209 -0
- package/package.json +107 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/replyonce.ts","../src/key.ts","../src/logic.ts","../src/stores/memory.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { effectKey, providerIdempotencyKey } from './key.js';\nimport * as logic from './logic.js';\nimport type { BeginOutcome, ClaimOutcome, ConflictMode, Policy } from './logic.js';\nimport type {\n EffectContext,\n EffectRecord,\n EffectStatus,\n ErrorClass,\n Json,\n OnceResult,\n ReconcileVerdict,\n ReplyOnceEvent,\n RunEndReason,\n RunMode,\n RunRecord,\n Store,\n} from './types.js';\n\n/** Throw this (or anything with `replyonce: 'not_applied'`) when the effect definitely did not happen. */\nexport class NotApplied extends Error {\n readonly replyonce = 'not_applied' as const;\n constructor(message = 'effect not applied', options?: { cause?: unknown }) {\n super(message);\n this.name = 'NotApplied';\n if (options && 'cause' in options) (this as { cause?: unknown }).cause = options.cause;\n }\n}\n\nexport function defaultClassifyError(err: unknown): ErrorClass {\n if (err instanceof NotApplied) return 'not_applied';\n if (err && typeof err === 'object' && (err as { replyonce?: unknown }).replyonce === 'not_applied') {\n return 'not_applied';\n }\n return 'ambiguous';\n}\n\nexport interface KeyParts {\n /** Usually the conversation or thread id. All fencing happens within a scope. */\n scope: string;\n /** What the effect is: `reply`, `refund`, `create_ticket`. */\n intent: string;\n /**\n * The business identity of the effect, hashed into the key. Use what makes it the same\n * action (the inbound message being answered, the order being refunded), never\n * model-generated text or a model tool-call id.\n */\n payload?: unknown;\n}\n\nexport type Reconciler = (entry: EffectRecord) => Promise<ReconcileVerdict>;\n\nexport interface ReplyOnceOptions {\n store: Store;\n /** How long a claim may stay pending before it is treated as abandoned (ambiguous). Default 15 min. */\n pendingTtlMs?: number;\n /** Attempts per key, counting only attempts known not to have applied. Default 3. */\n maxAttempts?: number;\n /** How long settled effects and ended runs are remembered. Default 7 days. */\n retentionMs?: number;\n /** Run lease length; renewed on every claim and heartbeat. Default 60 s. */\n leaseTtlMs?: number;\n /** Decide whether a thrown error means \"did not happen\" or \"might have happened\". */\n classifyError?: (err: unknown) => ErrorClass;\n /** Ask the outside world what happened to an ambiguous effect. */\n reconcile?: Reconciler;\n onEvent?: (event: ReplyOnceEvent) => void;\n clock?: () => number;\n}\n\nexport interface OnceOptions {\n /** Short human-readable description, shown to other runs in conflict reports. */\n summary?: string;\n classifyError?: (err: unknown) => ErrorClass;\n reconcile?: Reconciler;\n /** Wait for an in-flight duplicate to settle instead of returning `in_flight` immediately. */\n wait?: { timeoutMs: number; intervalMs?: number };\n}\n\nexport interface RunOnceOptions extends OnceOptions {\n /**\n * `any` (default): refuse if another run wrote anything on this scope since this run last\n * looked. `same-intent`: only effects with the same intent conflict. `none`: per-key\n * idempotency and supersession only.\n */\n conflict?: ConflictMode;\n /** Record that this run has now seen the conflicts, so its next attempt can proceed. Default true. */\n ack?: boolean;\n}\n\nexport interface CheckResult {\n /** `true` only when the effect is known to have happened. */\n done: boolean;\n status: EffectStatus | 'none';\n entry: EffectRecord | null;\n}\n\nexport interface BeginOptions {\n /** Stable id for this unit of work. Reuse it on redelivery (for example the queue job id). */\n runId?: string;\n mode?: RunMode;\n leaseTtlMs?: number;\n /**\n * Epoch ms when this run's context (the conversation it will act on) was read, for\n * example when the webhook payload was captured. Writes by other runs that landed\n * after this point are reported as `conversation_changed`. Default: now.\n */\n contextAt?: number;\n}\n\nexport type Effect<T> = (ctx: EffectContext) => Promise<T> | T;\n\nconst MAX_ROUNDS = 5;\n\nexport class ReplyOnce {\n readonly store: Store;\n readonly policy: Policy;\n readonly leaseTtlMs: number;\n private readonly classify: (err: unknown) => ErrorClass;\n private readonly reconciler: Reconciler | undefined;\n private readonly emit: (event: ReplyOnceEvent) => void;\n private readonly now: () => number;\n\n constructor(options: ReplyOnceOptions) {\n if (!options || !options.store) throw new TypeError('replyonce: a store is required');\n this.store = options.store;\n this.policy = {\n pendingTtlMs: options.pendingTtlMs ?? 15 * 60_000,\n maxAttempts: options.maxAttempts ?? 3,\n retentionMs: options.retentionMs ?? 7 * 24 * 3_600_000,\n };\n if (this.policy.pendingTtlMs <= 0 || this.policy.maxAttempts < 1 || this.policy.retentionMs <= 0) {\n throw new RangeError('replyonce: pendingTtlMs, maxAttempts and retentionMs must be positive');\n }\n this.leaseTtlMs = options.leaseTtlMs ?? 60_000;\n this.classify = options.classifyError ?? defaultClassifyError;\n this.reconciler = options.reconcile;\n const onEvent = options.onEvent;\n this.emit = onEvent\n ? (e) => {\n try {\n onEvent(e);\n } catch {\n // an observer must never change the outcome\n }\n }\n : () => {};\n this.now = options.clock ?? Date.now;\n }\n\n /** Resolve key parts to the ledger key used for them. */\n key(parts: KeyParts): string {\n return effectKey(parts.intent, parts.payload);\n }\n\n /**\n * Run `effect` at most once for this key. A string key is its own scope; key parts give\n * a scope (conversation) plus an intent and business identity.\n */\n once<T>(key: string | KeyParts, effect: Effect<T>, options: OnceOptions = {}): Promise<OnceResult<T>> {\n const { scope, ledgerKey, intent } = this.resolveKey(key);\n return this.execute(scope, ledgerKey, intent, null, effect, { ...options, conflict: 'none', ack: false });\n }\n\n /** \"Was this already done?\" — safe to expose to a model as a read-only tool. */\n async check(key: string | KeyParts): Promise<CheckResult> {\n const { scope, ledgerKey } = this.resolveKey(key);\n const state = await this.store.read(scope);\n const entry = state?.effects[ledgerKey] ?? null;\n if (!entry) return { done: false, status: 'none', entry: null };\n const status = logic.effectiveStatus(entry, this.now(), this.policy);\n return { done: status === 'committed', status, entry };\n }\n\n /** Every effect recorded on a scope, oldest first. */\n async history(scope: string): Promise<EffectRecord[]> {\n const state = await this.store.read(scope);\n if (!state) return [];\n const now = this.now();\n return Object.values(state.effects)\n .map((e) => ({ ...e, status: logic.effectiveStatus(e, now, this.policy) }))\n .sort((a, b) => a.createdAt - b.createdAt);\n }\n\n /** Record what an operator or reconciler found out about an ambiguous effect. */\n async resolve(\n key: string | KeyParts,\n verdict: { outcome: 'committed'; result?: Json } | { outcome: 'not_applied' },\n ): Promise<EffectRecord | null> {\n const { scope, ledgerKey } = this.resolveKey(key);\n const result = 'result' in verdict ? verdict.result : undefined;\n const now = this.now();\n const entry = await this.store.transact(scope, (s) =>\n logic.resolve(s, { key: ledgerKey, outcome: verdict.outcome, result, now }, this.policy),\n );\n if (entry) this.emit({ type: 'reconciled', scope, key: ledgerKey, outcome: verdict.outcome });\n return entry;\n }\n\n /**\n * Start (or, with the same `runId`, resume) a unit of work on a scope. The run gets a\n * fencing token; its writes are refused once a newer run owns the scope.\n */\n async begin(scope: string, options: BeginOptions = {}): Promise<Run> {\n assertScope(scope);\n const runId = options.runId ?? randomUUID();\n const leaseTtlMs = options.leaseTtlMs ?? this.leaseTtlMs;\n const now = this.now();\n const outcome = await this.store.transact(scope, (s) =>\n logic.begin(\n s,\n {\n runId,\n mode: options.mode ?? 'supersede',\n leaseTtlMs,\n now,\n ...(options.contextAt !== undefined ? { contextAt: options.contextAt } : {}),\n },\n this.policy,\n ),\n );\n return new Run(this, scope, runId, leaseTtlMs, outcome);\n }\n\n /** Cancel a run: its future writes are refused with `run_cancelled`. */\n async cancel(scope: string, runId: string): Promise<boolean> {\n const now = this.now();\n const r = await this.store.transact(scope, (s) =>\n logic.endRun(s, { runId, reason: 'cancelled', now }, this.policy),\n );\n return r !== null;\n }\n\n /** @internal */\n async renewRun(scope: string, runId: string, token: number, leaseTtlMs: number): Promise<boolean> {\n const now = this.now();\n const r = await this.store.transact(scope, (s) =>\n logic.renew(s, { runId, token, leaseTtlMs, now }, this.policy),\n );\n return r.ok;\n }\n\n /** @internal */\n async endRun(scope: string, runId: string, token: number, reason: RunEndReason): Promise<void> {\n const now = this.now();\n await this.store.transact(scope, (s) => logic.endRun(s, { runId, token, reason, now }, this.policy));\n }\n\n /** @internal */\n resolveKey(key: string | KeyParts): { scope: string; ledgerKey: string; intent: string } {\n if (typeof key === 'string') {\n assertScope(key);\n return { scope: key, ledgerKey: key, intent: 'once' };\n }\n assertScope(key.scope);\n return { scope: key.scope, ledgerKey: effectKey(key.intent, key.payload), intent: key.intent };\n }\n\n /** @internal */\n async execute<T>(\n scope: string,\n key: string,\n intent: string,\n run: { runId: string; token: number; leaseTtlMs: number } | null,\n effect: Effect<T>,\n options: RunOnceOptions,\n ): Promise<OnceResult<T>> {\n const runId = run?.runId ?? null;\n const token = run?.token ?? null;\n const classify = options.classifyError ?? this.classify;\n const reconcile = options.reconcile ?? this.reconciler;\n const waitUntil = options.wait ? this.now() + options.wait.timeoutMs : 0;\n let reconciledOnce = false;\n\n for (let round = 0; round < MAX_ROUNDS; round++) {\n const attemptId = randomUUID();\n const now = this.now();\n const claimed: ClaimOutcome = await this.store.transact(scope, (s) =>\n logic.claim(\n s,\n scope,\n {\n key,\n intent,\n summary: options.summary ?? null,\n runId,\n token,\n attemptId,\n conflict: options.conflict ?? 'any',\n ack: options.ack ?? true,\n leaseTtlMs: run?.leaseTtlMs ?? this.leaseTtlMs,\n now,\n },\n this.policy,\n ),\n );\n\n switch (claimed.kind) {\n case 'replay':\n this.emit({ type: 'replayed', scope, key });\n return { status: 'replayed', ok: true, value: claimed.entry.result as T, key, entry: claimed.entry };\n\n case 'refused':\n this.emit({ type: 'refused', scope, key, reason: claimed.reason, runId });\n return {\n status: 'refused',\n ok: false,\n key,\n reason: claimed.reason,\n ...(claimed.conflicts ? { conflicts: claimed.conflicts } : {}),\n ...(claimed.entry ? { entry: claimed.entry } : {}),\n };\n\n case 'in_flight': {\n if (options.wait && this.now() < waitUntil) {\n await this.waitForSettle(scope, key, waitUntil, options.wait.intervalMs ?? 100);\n continue;\n }\n this.emit({ type: 'in_flight', scope, key });\n return { status: 'in_flight', ok: false, key, entry: claimed.entry };\n }\n\n case 'ambiguous': {\n if (reconcile && !reconciledOnce) {\n reconciledOnce = true;\n let verdict: ReconcileVerdict;\n try {\n verdict = await reconcile(claimed.entry);\n } catch {\n verdict = { outcome: 'unknown' };\n }\n if (verdict.outcome !== 'unknown') {\n const at = this.now();\n const resolved = await this.store.transact(scope, (s) =>\n logic.resolve(\n s,\n {\n key,\n outcome: verdict.outcome as 'committed' | 'not_applied',\n result: verdict.outcome === 'committed' ? verdict.result : undefined,\n attemptId: claimed.entry.attemptId,\n now: at,\n },\n this.policy,\n ),\n );\n if (resolved) this.emit({ type: 'reconciled', scope, key, outcome: verdict.outcome });\n continue;\n }\n }\n this.emit({ type: 'ambiguous', scope, key });\n return { status: 'ambiguous', ok: false, key, entry: claimed.entry };\n }\n\n case 'acquired':\n return this.runAcquired(scope, key, claimed.entry, effect, classify);\n }\n }\n const entry = (await this.store.read(scope))?.effects[key];\n if (entry && entry.status === 'committed') {\n return { status: 'replayed', ok: true, value: entry.result as T, key, entry };\n }\n if (entry) {\n this.emit({ type: 'in_flight', scope, key });\n return { status: 'in_flight', ok: false, key, entry };\n }\n throw new Error('replyonce: could not claim key after several rounds');\n }\n\n private async runAcquired<T>(\n scope: string,\n key: string,\n entry: EffectRecord,\n effect: Effect<T>,\n classify: (err: unknown) => ErrorClass,\n ): Promise<OnceResult<T>> {\n const { attempt, attemptId, runId } = entry;\n this.emit({ type: 'claimed', scope, key, attempt, runId });\n const remaining = Math.max(1, entry.claimedAt + this.policy.pendingTtlMs - this.now());\n const ctx: EffectContext = {\n key,\n scope,\n intent: entry.intent,\n attempt,\n idempotencyKey: providerIdempotencyKey(scope, key),\n fencingToken: entry.token,\n signal: AbortSignal.timeout(remaining),\n };\n\n let value: T;\n try {\n value = await effect(ctx);\n } catch (error) {\n const cls = safeClassify(classify, error);\n const outcome = cls === 'not_applied' ? 'failed' : 'ambiguous';\n await this.settleWithRetry(scope, {\n key,\n attemptId,\n outcome,\n error: describeError(error),\n now: 0,\n }).catch(() => undefined);\n if (outcome === 'failed') {\n this.emit({ type: 'failed', scope, key, attempt, error });\n return {\n status: 'failed',\n ok: false,\n key,\n attempt,\n error,\n retryable: attempt < this.policy.maxAttempts,\n };\n }\n this.emit({ type: 'ambiguous', scope, key, error });\n const current = (await this.store.read(scope).catch(() => null))?.effects[key] ?? entry;\n return { status: 'ambiguous', ok: false, key, entry: current, error };\n }\n\n let stored: Json | undefined;\n try {\n stored = value === undefined ? null : (JSON.parse(JSON.stringify(value)) as Json);\n } catch {\n stored = null;\n this.emit({ type: 'unserializable_result', scope, key });\n }\n try {\n const settled = await this.settleWithRetry(scope, {\n key,\n attemptId,\n outcome: 'committed',\n result: stored,\n now: 0,\n });\n this.emit({ type: 'committed', scope, key, attempt, runId });\n if (settled.kind === 'stale' || settled.wasAbandoned) {\n this.emit({ type: 'late_settle', scope, key, attempt });\n return { status: 'executed', ok: true, value, key, attempt, lateSettle: true };\n }\n } catch {\n // The effect happened but the ledger could not record it. The claim stays pending and\n // later reads treat it as ambiguous, which fails safe.\n this.emit({ type: 'late_settle', scope, key, attempt });\n return { status: 'executed', ok: true, value, key, attempt, lateSettle: true };\n }\n return { status: 'executed', ok: true, value, key, attempt };\n }\n\n private async settleWithRetry(scope: string, input: logic.SettleInput): Promise<logic.SettleOutcome> {\n let lastError: unknown;\n for (let i = 0; i < 4; i++) {\n try {\n const now = this.now();\n return await this.store.transact(scope, (s) => logic.settle(s, { ...input, now }, this.policy));\n } catch (err) {\n lastError = err;\n await sleep(25 * 4 ** i);\n }\n }\n throw lastError;\n }\n\n private async waitForSettle(scope: string, key: string, until: number, intervalMs: number): Promise<void> {\n while (this.now() < until) {\n await sleep(Math.min(intervalMs, Math.max(1, until - this.now())));\n const e = (await this.store.read(scope))?.effects[key];\n if (!e || logic.effectiveStatus(e, this.now(), this.policy) !== 'pending') return;\n }\n }\n}\n\nexport type RunState = BeginOutcome['state'];\n\n/** A unit of agent work on one scope, holding a fencing token. */\nexport class Run {\n readonly scope: string;\n readonly runId: string;\n /** `new`, `resumed` (same runId seen before), `superseded`, `ended`, or `busy` (exclusive mode). */\n readonly state: RunState;\n /** Monotonic per scope. `null` when the run could not start (`busy`). */\n readonly fencingToken: number | null;\n /** Effects this runId already claimed before (non-empty on redelivery). */\n readonly priorEffects: EffectRecord[];\n readonly busyHolder: { runId: string; token: number; leaseExpiresAt: number } | null;\n private readonly ro: ReplyOnce;\n private readonly leaseTtlMs: number;\n\n constructor(ro: ReplyOnce, scope: string, runId: string, leaseTtlMs: number, outcome: BeginOutcome) {\n this.ro = ro;\n this.scope = scope;\n this.runId = runId;\n this.leaseTtlMs = leaseTtlMs;\n this.state = outcome.state;\n if (outcome.state === 'busy') {\n this.fencingToken = null;\n this.priorEffects = [];\n this.busyHolder = outcome.holder;\n } else {\n this.fencingToken = (outcome.run as RunRecord).token;\n this.priorEffects = outcome.priorEffects;\n this.busyHolder = null;\n }\n }\n\n /** `true` when this run may write: it is new or resumed, not superseded, ended or busy. */\n get canWrite(): boolean {\n return this.state === 'new' || this.state === 'resumed';\n }\n\n /**\n * `true` when a previous delivery of this run already claimed a write that may have\n * reached the outside world (pending, committed or ambiguous).\n */\n get alreadyWrote(): boolean {\n return this.priorEffects.some((e) => e.status !== 'failed');\n }\n\n /** Run `effect` at most once per (scope, intent, payload), fenced to this run. */\n once<T>(intent: string, payload: unknown, effect: Effect<T>, options: RunOnceOptions = {}): Promise<OnceResult<T>> {\n const key = effectKey(intent, payload);\n if (this.fencingToken === null) {\n return Promise.resolve({ status: 'refused', ok: false, key, reason: 'busy' });\n }\n const self = { runId: this.runId, token: this.fencingToken, leaseTtlMs: this.leaseTtlMs };\n return this.ro.execute(this.scope, key, intent, self, effect, options);\n }\n\n check(intent: string, payload?: unknown): Promise<CheckResult> {\n return this.ro.check({ scope: this.scope, intent, payload });\n }\n\n history(): Promise<EffectRecord[]> {\n return this.ro.history(this.scope);\n }\n\n /**\n * Renew the lease. `false` means stop: the run was superseded (by a newer run or a later\n * delivery of this one), cancelled or ended.\n */\n heartbeat(): Promise<boolean> {\n if (this.fencingToken === null) return Promise.resolve(false);\n return this.ro.renewRun(this.scope, this.runId, this.fencingToken, this.leaseTtlMs);\n }\n\n /** Mark the run finished. A no-op if a later delivery of the same run has taken over. */\n end(reason: RunEndReason = 'completed'): Promise<void> {\n if (this.fencingToken === null) return Promise.resolve();\n return this.ro.endRun(this.scope, this.runId, this.fencingToken, reason);\n }\n}\n\nexport function createReplyOnce(options: ReplyOnceOptions): ReplyOnce {\n return new ReplyOnce(options);\n}\n\n/** Return the value of a successful result, or throw a `ReplyOnceError` describing why not. */\nexport function unwrap<T>(result: OnceResult<T>): T {\n if (result.ok) return result.value;\n throw new ReplyOnceError(result);\n}\n\nexport class ReplyOnceError extends Error {\n readonly result: Exclude<OnceResult<unknown>, { ok: true }>;\n constructor(result: Exclude<OnceResult<unknown>, { ok: true }>) {\n const detail = result.status === 'refused' ? `${result.status}: ${result.reason}` : result.status;\n super(`replyonce: effect ${result.key} not executed (${detail})`);\n this.name = 'ReplyOnceError';\n this.result = result;\n }\n}\n\nfunction assertScope(scope: string): void {\n if (typeof scope !== 'string' || scope.length === 0 || scope.length > 512) {\n throw new TypeError('replyonce: scope must be a non-empty string of at most 512 characters');\n }\n}\n\nfunction safeClassify(classify: (err: unknown) => ErrorClass, err: unknown): ErrorClass {\n try {\n return classify(err) === 'not_applied' ? 'not_applied' : 'ambiguous';\n } catch {\n return 'ambiguous';\n }\n}\n\nfunction describeError(err: unknown): string {\n const text = err instanceof Error ? `${err.name}: ${err.message}` : String(err);\n return text.slice(0, 500);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n","import { createHash } from 'node:crypto';\n\n/** Deterministic JSON: object keys sorted, `undefined` members dropped. */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(normalize(value, new Set()));\n}\n\nfunction normalize(value: unknown, seen: Set<unknown>): unknown {\n if (value === null || value === undefined) return null;\n if (typeof value === 'bigint') return value.toString();\n if (typeof value !== 'object') {\n if (typeof value === 'number' && !Number.isFinite(value)) return String(value);\n if (typeof value === 'function' || typeof value === 'symbol') return null;\n return value;\n }\n if (value instanceof Date) return value.toISOString();\n if (seen.has(value)) throw new TypeError('replyonce: cannot hash a circular payload');\n seen.add(value);\n let out: unknown;\n if (Array.isArray(value)) {\n out = value.map((v) => normalize(v, seen));\n } else {\n const obj: Record<string, unknown> = {};\n for (const k of Object.keys(value as Record<string, unknown>).sort()) {\n const v = (value as Record<string, unknown>)[k];\n if (v === undefined) continue;\n obj[k] = normalize(v, seen);\n }\n out = obj;\n }\n seen.delete(value);\n return out;\n}\n\nexport function sha256Hex(input: string): string {\n return createHash('sha256').update(input).digest('hex');\n}\n\nexport function hashPayload(payload: unknown): string {\n return sha256Hex(canonicalJson(payload)).slice(0, 32);\n}\n\nconst SAFE_INTENT = /^[A-Za-z0-9_.:-]{1,100}$/;\n\n/** Ledger key for an intent plus the business identity of the payload. */\nexport function effectKey(intent: string, payload: unknown): string {\n if (!SAFE_INTENT.test(intent)) {\n throw new TypeError(\n `replyonce: intent must match ${SAFE_INTENT} (got ${JSON.stringify(intent)})`,\n );\n }\n return payload === undefined ? intent : `${intent}:${hashPayload(payload)}`;\n}\n\n/** Stable idempotency key to hand to a provider (Stripe-style `Idempotency-Key`). */\nexport function providerIdempotencyKey(scope: string, key: string): string {\n return `ro_${sha256Hex(`${scope}\\u0000${key}`).slice(0, 40)}`;\n}\n","import type {\n ConflictInfo,\n EffectRecord,\n Json,\n RefusalReason,\n RunEndReason,\n RunMode,\n RunRecord,\n ScopeState,\n TransactResult,\n} from './types.js';\n\n/**\n * The replyonce state machine. Every function here is pure: it takes a scope's state and\n * returns the next state plus a decision. Stores only make each call atomic.\n */\n\nexport interface Policy {\n pendingTtlMs: number;\n maxAttempts: number;\n retentionMs: number;\n}\n\nexport type ConflictMode = 'any' | 'same-intent' | 'none';\n\nexport const SUMMARY_MAX = 2000;\n\nexport function emptyState(): ScopeState {\n return { v: 1, seq: 0, tokenSeq: 0, runs: {}, effects: {} };\n}\n\nfunction load(state: ScopeState | null, now: number, policy: Policy): ScopeState {\n const s = state ? (structuredClone(state) as ScopeState) : emptyState();\n if (s.v !== 1) throw new Error(`replyonce: unsupported state version ${String(s.v)}`);\n const horizon = now - policy.retentionMs;\n for (const [k, e] of Object.entries(s.effects)) {\n if ((e.status === 'committed' || e.status === 'failed') && (e.settledAt ?? e.claimedAt) < horizon) {\n delete s.effects[k];\n }\n }\n for (const [id, r] of Object.entries(s.runs)) {\n if ((r.endedAt ?? r.leaseExpiresAt) < horizon) delete s.runs[id];\n }\n return s;\n}\n\n/** Advance the scope's logical clock for a change being made now. */\nfunction tick(s: ScopeState): number {\n s.seq += 1;\n return s.seq;\n}\n\nexport function isAbandoned(e: EffectRecord, now: number, policy: Policy): boolean {\n return e.status === 'pending' && now - e.claimedAt >= policy.pendingTtlMs;\n}\n\n/** The status a reader should act on: a pending claim past its TTL is treated as ambiguous. */\nexport function effectiveStatus(e: EffectRecord, now: number, policy: Policy): EffectRecord['status'] {\n return isAbandoned(e, now, policy) ? 'ambiguous' : e.status;\n}\n\nfunction isLive(r: RunRecord, now: number): boolean {\n return r.endedAt === null && r.leaseExpiresAt > now;\n}\n\n/**\n * Is `a` newer than `b`? Exclusive leases order by token; otherwise by context recency. Never\n * by token there: a redelivery bumps an old run's token. Equal context times order by when\n * the run first began only if neither run gave a `contextAt`, because then each read its\n * context as it began. Otherwise neither is newer: begin order says nothing about whose\n * context is fresher, and fencing out the wrong one would leave its message unanswered.\n * Both may then write; conflict checks keep them from answering the same thing twice.\n */\nfunction isNewer(a: RunRecord, b: RunRecord): boolean {\n if (a.mode === 'exclusive' && b.mode === 'exclusive') return a.token > b.token;\n if (a.orderAt !== b.orderAt) return a.orderAt > b.orderAt;\n if (a.baseContextAt !== null || b.baseContextAt !== null) return false;\n return a.baseSeq > b.baseSeq;\n}\n\n/**\n * A newer run owns the scope if it is live, or if it already wrote (it may have answered).\n * A newer run that ended without writing, or was cancelled, does not.\n */\nexport function supersedingRun(s: ScopeState, run: RunRecord, now: number): RunRecord | null {\n let best: RunRecord | null = null;\n for (const r of Object.values(s.runs)) {\n if (r.runId === run.runId || r.endReason === 'cancelled' || !isNewer(r, run)) continue;\n if (r.wrote || isLive(r, now)) {\n if (!best || isNewer(r, best)) best = r;\n }\n }\n return best;\n}\n\nfunction markSeen(run: RunRecord, seq: number, now: number): void {\n run.seenSeq = Math.max(run.seenSeq, seq);\n run.seenAt = Math.max(run.seenAt, now);\n run.contextAt = null;\n}\n\n// ---------------------------------------------------------------- begin\n\nexport interface BeginInput {\n runId: string;\n mode: RunMode;\n leaseTtlMs: number;\n now: number;\n /** When the run's view of the scope was read. Writes that landed after it are conflicts. */\n contextAt?: number;\n}\n\nexport type BeginOutcome =\n | { state: 'new' | 'resumed' | 'superseded' | 'ended'; run: RunRecord; priorEffects: EffectRecord[] }\n | { state: 'busy'; holder: { runId: string; token: number; leaseExpiresAt: number } };\n\nexport function begin(\n state: ScopeState | null,\n input: BeginInput,\n policy: Policy,\n): TransactResult<BeginOutcome> {\n const { now } = input;\n const s = load(state, now, policy);\n const existing = s.runs[input.runId];\n if (existing) {\n const priorEffects = Object.values(s.effects).filter((e) => e.runId === input.runId);\n if (existing.endedAt !== null) {\n return { next: null, result: { state: 'ended', run: existing, priorEffects } };\n }\n existing.leaseExpiresAt = Math.max(existing.leaseExpiresAt, now + input.leaseTtlMs);\n tick(s);\n // A redelivery takes the run over; any earlier delivery still executing is fenced out.\n s.tokenSeq += 1;\n existing.token = s.tokenSeq;\n // It acts on the run's original context, so it has not seen what earlier deliveries saw,\n // unless it says it re-read the scope.\n existing.seenSeq = existing.baseSeq;\n existing.seenAt = existing.baseContextAt ?? existing.startedAt;\n existing.contextAt = existing.baseContextAt;\n if (input.contextAt !== undefined && input.contextAt > existing.seenAt) {\n const fresh = Math.min(input.contextAt, now);\n for (const e of Object.values(s.effects)) {\n if (e.settleSeq !== null && e.settledAt !== null && e.settledAt < fresh) {\n existing.seenSeq = Math.max(existing.seenSeq, e.settleSeq);\n }\n }\n existing.seenAt = fresh;\n existing.contextAt = fresh;\n }\n const newer = supersedingRun(s, existing, now);\n return { next: s, result: { state: newer ? 'superseded' : 'resumed', run: existing, priorEffects } };\n }\n if (input.mode === 'exclusive') {\n let holder: RunRecord | undefined;\n for (const r of Object.values(s.runs)) {\n if (isLive(r, now) && (!holder || r.token > holder.token)) holder = r;\n }\n if (holder) {\n return {\n next: null,\n result: {\n state: 'busy',\n holder: { runId: holder.runId, token: holder.token, leaseExpiresAt: holder.leaseExpiresAt },\n },\n };\n }\n }\n const seq = tick(s);\n s.tokenSeq += 1;\n const contextAt = input.contextAt === undefined ? null : Math.min(input.contextAt, now);\n const run: RunRecord = {\n runId: input.runId,\n token: s.tokenSeq,\n mode: input.mode,\n startedAt: now,\n orderAt: input.mode === 'exclusive' ? now : (contextAt ?? now),\n leaseExpiresAt: now + input.leaseTtlMs,\n seenSeq: seq,\n seenAt: contextAt ?? now,\n contextAt,\n baseSeq: seq,\n baseContextAt: contextAt,\n wrote: false,\n endedAt: null,\n endReason: null,\n };\n s.runs[input.runId] = run;\n const newer = supersedingRun(s, run, now);\n return { next: s, result: { state: newer ? 'superseded' : 'new', run, priorEffects: [] } };\n}\n\n// ---------------------------------------------------------------- heartbeat / end\n\nexport type RenewOutcome =\n | { ok: true; run: RunRecord }\n | { ok: false; reason: 'unknown_run' | 'run_ended' | 'run_cancelled' | 'superseded' };\n\nexport function renew(\n state: ScopeState | null,\n input: { runId: string; token: number; leaseTtlMs: number; now: number },\n policy: Policy,\n): TransactResult<RenewOutcome> {\n const s = load(state, input.now, policy);\n const r = s.runs[input.runId];\n if (!r) return { next: null, result: { ok: false, reason: 'unknown_run' } };\n if (r.token !== input.token) return { next: null, result: { ok: false, reason: 'superseded' } };\n if (r.endReason === 'cancelled') return { next: null, result: { ok: false, reason: 'run_cancelled' } };\n if (r.endedAt !== null) return { next: null, result: { ok: false, reason: 'run_ended' } };\n if (supersedingRun(s, r, input.now)) return { next: null, result: { ok: false, reason: 'superseded' } };\n r.leaseExpiresAt = Math.max(r.leaseExpiresAt, input.now + input.leaseTtlMs);\n tick(s);\n return { next: s, result: { ok: true, run: r } };\n}\n\nexport function endRun(\n state: ScopeState | null,\n input: { runId: string; reason: RunEndReason; now: number; /** Ignore if a later delivery holds the run. */ token?: number },\n policy: Policy,\n): TransactResult<RunRecord | null> {\n const s = load(state, input.now, policy);\n const r = s.runs[input.runId];\n if (!r) return { next: null, result: null };\n if (input.token !== undefined && r.token !== input.token) return { next: null, result: null };\n if (r.endedAt !== null && !(input.reason === 'cancelled' && r.endReason !== 'cancelled')) {\n return { next: null, result: r };\n }\n r.endedAt = r.endedAt ?? input.now;\n r.endReason = input.reason;\n tick(s);\n return { next: s, result: r };\n}\n\n// ---------------------------------------------------------------- claim\n\nexport interface ClaimInput {\n key: string;\n intent: string;\n summary: string | null;\n runId: string | null;\n /** The claiming delivery's fencing token. A stale token is refused as superseded. */\n token: number | null;\n attemptId: string;\n conflict: ConflictMode;\n ack: boolean;\n leaseTtlMs: number;\n now: number;\n}\n\nexport type ClaimOutcome =\n | { kind: 'acquired'; entry: EffectRecord }\n | { kind: 'replay'; entry: EffectRecord }\n | { kind: 'in_flight'; entry: EffectRecord }\n | { kind: 'ambiguous'; entry: EffectRecord }\n | { kind: 'refused'; reason: RefusalReason; conflicts?: ConflictInfo[]; entry?: EffectRecord };\n\n/** Writes by others on the scope that `run` has not seen yet, or that may still land. */\nexport function conflictsFor(\n s: ScopeState,\n run: RunRecord,\n key: string,\n intent: string,\n mode: ConflictMode,\n now: number,\n policy: Policy,\n): ConflictInfo[] {\n if (mode === 'none') return [];\n const out: ConflictInfo[] = [];\n for (const e of Object.values(s.effects)) {\n if (e.key === key) continue;\n if (e.runId === run.runId) {\n // This delivery knows its own writes. An earlier delivery's writes matter only when\n // this one is about to do the same kind of thing differently (a reply worded anew).\n if (e.token === run.token || e.intent !== intent) continue;\n }\n if (mode === 'same-intent' && e.intent !== intent) continue;\n const abandoned = isAbandoned(e, now, policy);\n let conflict = false;\n if (e.status === 'pending' && !abandoned) {\n conflict = true;\n } else if (abandoned) {\n // It may have landed at any time up to when it became abandoned.\n conflict = e.claimedAt + policy.pendingTtlMs >= run.seenAt;\n } else if (e.status === 'committed' || e.status === 'ambiguous') {\n conflict =\n (e.settleSeq ?? 0) > run.seenSeq || (run.contextAt !== null && (e.settledAt ?? 0) >= run.contextAt);\n }\n if (conflict) {\n out.push({\n key: e.key,\n intent: e.intent,\n status: abandoned ? 'ambiguous' : e.status,\n runId: e.runId,\n summary: e.summary,\n at: e.settledAt ?? e.claimedAt,\n });\n }\n }\n return out.sort((a, b) => a.at - b.at).slice(0, 10);\n}\n\nexport function claim(\n state: ScopeState | null,\n scope: string,\n input: ClaimInput,\n policy: Policy,\n): TransactResult<ClaimOutcome> {\n const { now } = input;\n const s = load(state, now, policy);\n const prior = s.effects[input.key];\n\n if (prior) {\n if (prior.status === 'committed') return { next: null, result: { kind: 'replay', entry: prior } };\n if (prior.status === 'pending') {\n if (!isAbandoned(prior, now, policy)) {\n return { next: null, result: { kind: 'in_flight', entry: prior } };\n }\n const seq = tick(s);\n prior.status = 'ambiguous';\n prior.settledAt = prior.claimedAt + policy.pendingTtlMs;\n prior.settleSeq = seq;\n prior.error = prior.error ?? 'abandoned: not settled within pendingTtlMs';\n return { next: s, result: { kind: 'ambiguous', entry: prior } };\n }\n if (prior.status === 'ambiguous') return { next: null, result: { kind: 'ambiguous', entry: prior } };\n if (prior.attempt >= policy.maxAttempts) {\n return { next: null, result: { kind: 'refused', reason: 'exhausted', entry: prior } };\n }\n }\n\n let run: RunRecord | undefined;\n if (input.runId !== null) {\n run = s.runs[input.runId];\n if (!run) return { next: null, result: { kind: 'refused', reason: 'unknown_run' } };\n if (run.endReason === 'cancelled') return { next: null, result: { kind: 'refused', reason: 'run_cancelled' } };\n if (run.endedAt !== null) return { next: null, result: { kind: 'refused', reason: 'run_ended' } };\n if ((input.token !== null && run.token !== input.token) || supersedingRun(s, run, now)) {\n return { next: null, result: { kind: 'refused', reason: 'superseded' } };\n }\n\n const conflicts = conflictsFor(s, run, input.key, input.intent, input.conflict, now, policy);\n if (conflicts.length > 0) {\n if (!input.ack) {\n return { next: null, result: { kind: 'refused', reason: 'conversation_changed', conflicts } };\n }\n markSeen(run, tick(s), now);\n return { next: s, result: { kind: 'refused', reason: 'conversation_changed', conflicts } };\n }\n }\n\n const seq = tick(s);\n const entry: EffectRecord = {\n key: input.key,\n scope,\n intent: input.intent,\n status: 'pending',\n attempt: (prior?.attempt ?? 0) + 1,\n attemptId: input.attemptId,\n runId: input.runId,\n token: run?.token ?? null,\n summary: input.summary === null ? null : input.summary.slice(0, SUMMARY_MAX),\n error: null,\n createdAt: prior?.createdAt ?? now,\n claimedAt: now,\n settledAt: null,\n claimSeq: seq,\n settleSeq: null,\n };\n s.effects[input.key] = entry;\n if (run) {\n // A claim does not mark anything seen: writes this check deliberately skipped (other\n // intents, or `none`) must still be reported to a later check that covers them.\n run.wrote = true;\n run.leaseExpiresAt = Math.max(run.leaseExpiresAt, now + input.leaseTtlMs);\n }\n return { next: s, result: { kind: 'acquired', entry } };\n}\n\n// ---------------------------------------------------------------- settle / resolve\n\nexport interface SettleInput {\n key: string;\n attemptId: string;\n outcome: 'committed' | 'failed' | 'ambiguous';\n result?: Json;\n error?: string | null;\n now: number;\n}\n\nexport type SettleOutcome =\n | { kind: 'settled'; entry: EffectRecord; wasAbandoned: boolean }\n | { kind: 'stale'; entry: EffectRecord | null };\n\nexport function settle(\n state: ScopeState | null,\n input: SettleInput,\n policy: Policy,\n): TransactResult<SettleOutcome> {\n const s = load(state, input.now, policy);\n const e = s.effects[input.key];\n // A late \"committed\" from the attempt a reconciler judged not applied is still the truth.\n const lateTruth = e?.status === 'failed' && input.outcome === 'committed';\n if (\n !e ||\n e.attemptId !== input.attemptId ||\n (e.status !== 'pending' && e.status !== 'ambiguous' && !lateTruth)\n ) {\n return { next: null, result: { kind: 'stale', entry: e ?? null } };\n }\n const wasAbandoned = e.status !== 'pending' || isAbandoned(e, input.now, policy);\n e.status = input.outcome;\n e.settledAt = input.now;\n e.settleSeq = tick(s);\n e.error = input.error ?? null;\n if (input.outcome === 'committed' && input.result !== undefined) e.result = input.result;\n return { next: s, result: { kind: 'settled', entry: e, wasAbandoned } };\n}\n\nexport interface ResolveInput {\n key: string;\n outcome: 'committed' | 'not_applied';\n result?: Json;\n /** Only resolve if the ledger still holds this attempt. */\n attemptId?: string;\n now: number;\n}\n\n/** Resolve an ambiguous (or abandoned) effect after checking the outside world. */\nexport function resolve(\n state: ScopeState | null,\n input: ResolveInput,\n policy: Policy,\n): TransactResult<EffectRecord | null> {\n const s = load(state, input.now, policy);\n const e = s.effects[input.key];\n if (!e) return { next: null, result: null };\n if (input.attemptId !== undefined && e.attemptId !== input.attemptId) return { next: null, result: null };\n if (effectiveStatus(e, input.now, policy) !== 'ambiguous') return { next: null, result: null };\n e.settledAt = input.now;\n e.settleSeq = tick(s);\n if (input.outcome === 'committed') {\n e.status = 'committed';\n e.error = null;\n if (input.result !== undefined) e.result = input.result;\n } else {\n e.status = 'failed';\n e.error = 'reconciled: not applied';\n }\n return { next: s, result: e };\n}\n","import type { ScopeState, Store, TransactResult } from '../types.js';\n\n/**\n * Single-process store. Atomic because `transact` reads, decides and writes in one\n * synchronous step. State is kept as JSON so it behaves like the shared stores.\n * Use it for tests, demos and single-instance deployments; it is lost on restart.\n */\nexport class MemoryStore implements Store {\n private readonly data = new Map<string, string>();\n\n async transact<T>(scope: string, fn: (state: ScopeState | null) => TransactResult<T>): Promise<T> {\n const raw = this.data.get(scope);\n const { next, result } = fn(raw === undefined ? null : (JSON.parse(raw) as ScopeState));\n if (next) this.data.set(scope, JSON.stringify(next));\n return result;\n }\n\n async read(scope: string): Promise<ScopeState | null> {\n const raw = this.data.get(scope);\n return raw === undefined ? null : (JSON.parse(raw) as ScopeState);\n }\n\n /** Number of scopes held. */\n get size(): number {\n return this.data.size;\n }\n\n clear(): void {\n this.data.clear();\n }\n}\n"],"mappings":"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","names":["newer","seq"]}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/stores/postgres.ts
|
|
21
|
+
var postgres_exports = {};
|
|
22
|
+
__export(postgres_exports, {
|
|
23
|
+
PostgresStore: () => PostgresStore,
|
|
24
|
+
postgresSchemaSql: () => postgresSchemaSql
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(postgres_exports);
|
|
27
|
+
var IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
28
|
+
function quoteTable(table) {
|
|
29
|
+
const parts = table.split(".");
|
|
30
|
+
if (parts.length > 2 || !parts.every((p) => IDENT.test(p))) {
|
|
31
|
+
throw new TypeError(`replyonce: invalid table name ${JSON.stringify(table)}`);
|
|
32
|
+
}
|
|
33
|
+
return parts.map((p) => `"${p}"`).join(".");
|
|
34
|
+
}
|
|
35
|
+
function postgresSchemaSql(table = "replyonce_scopes") {
|
|
36
|
+
const t = quoteTable(table);
|
|
37
|
+
return [
|
|
38
|
+
`CREATE TABLE IF NOT EXISTS ${t} (`,
|
|
39
|
+
` scope text PRIMARY KEY,`,
|
|
40
|
+
` state jsonb NOT NULL,`,
|
|
41
|
+
` version bigint NOT NULL DEFAULT 0,`,
|
|
42
|
+
` updated_at timestamptz NOT NULL DEFAULT now()`,
|
|
43
|
+
`);`
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
var PostgresStore = class {
|
|
47
|
+
pool;
|
|
48
|
+
table;
|
|
49
|
+
tableName;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
if (!options?.pool) throw new TypeError("replyonce: PostgresStore needs a pool");
|
|
52
|
+
this.pool = options.pool;
|
|
53
|
+
this.tableName = options.table ?? "replyonce_scopes";
|
|
54
|
+
this.table = quoteTable(this.tableName);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Creates the table if it does not exist. Safe to call from several workers at once:
|
|
58
|
+
* `CREATE TABLE IF NOT EXISTS` alone is not, because two sessions can both pass the
|
|
59
|
+
* existence check and one then fails on the catalog's unique index, so concurrent
|
|
60
|
+
* migrations of the same table are serialized with a transaction-scoped advisory lock.
|
|
61
|
+
*/
|
|
62
|
+
async migrate() {
|
|
63
|
+
const client = await this.pool.connect();
|
|
64
|
+
let broken;
|
|
65
|
+
try {
|
|
66
|
+
await client.query("BEGIN");
|
|
67
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`replyonce:migrate:${this.tableName}`]);
|
|
68
|
+
await client.query(postgresSchemaSql(this.tableName));
|
|
69
|
+
await client.query("COMMIT");
|
|
70
|
+
} catch (err) {
|
|
71
|
+
try {
|
|
72
|
+
await client.query("ROLLBACK");
|
|
73
|
+
} catch (rollbackErr) {
|
|
74
|
+
broken = rollbackErr;
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
} finally {
|
|
78
|
+
client.release(broken);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async read(scope) {
|
|
82
|
+
const { rows } = await this.pool.query(`SELECT state FROM ${this.table} WHERE scope = $1`, [scope]);
|
|
83
|
+
return rows[0]?.state ?? null;
|
|
84
|
+
}
|
|
85
|
+
async transact(scope, fn) {
|
|
86
|
+
const client = await this.pool.connect();
|
|
87
|
+
let broken;
|
|
88
|
+
try {
|
|
89
|
+
await client.query("BEGIN");
|
|
90
|
+
let { rows } = await client.query(`SELECT state FROM ${this.table} WHERE scope = $1 FOR UPDATE`, [scope]);
|
|
91
|
+
let exists = rows.length > 0;
|
|
92
|
+
const { next, result } = fn(exists ? rows[0].state ?? null : null);
|
|
93
|
+
if (next) {
|
|
94
|
+
if (!exists) {
|
|
95
|
+
const ins = await client.query(
|
|
96
|
+
`INSERT INTO ${this.table} (scope, state) VALUES ($1, $2::jsonb) ON CONFLICT (scope) DO NOTHING RETURNING scope`,
|
|
97
|
+
[scope, JSON.stringify(next)]
|
|
98
|
+
);
|
|
99
|
+
if (ins.rows.length === 0) {
|
|
100
|
+
({ rows } = await client.query(`SELECT state FROM ${this.table} WHERE scope = $1 FOR UPDATE`, [scope]));
|
|
101
|
+
exists = true;
|
|
102
|
+
const again = fn(rows[0]?.state ?? null);
|
|
103
|
+
if (again.next) {
|
|
104
|
+
await client.query(
|
|
105
|
+
`UPDATE ${this.table} SET state = $2::jsonb, version = version + 1, updated_at = now() WHERE scope = $1`,
|
|
106
|
+
[scope, JSON.stringify(again.next)]
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
await client.query("COMMIT");
|
|
110
|
+
return again.result;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
await client.query(
|
|
114
|
+
`UPDATE ${this.table} SET state = $2::jsonb, version = version + 1, updated_at = now() WHERE scope = $1`,
|
|
115
|
+
[scope, JSON.stringify(next)]
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
await client.query("COMMIT");
|
|
120
|
+
return result;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
try {
|
|
123
|
+
await client.query("ROLLBACK");
|
|
124
|
+
} catch (rollbackErr) {
|
|
125
|
+
broken = rollbackErr instanceof Error ? rollbackErr : new Error(String(rollbackErr));
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
} finally {
|
|
129
|
+
client.release(broken);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Delete scopes untouched for longer than `olderThanMs`. Returns the number removed. */
|
|
133
|
+
async purge(olderThanMs) {
|
|
134
|
+
const { rows } = await this.pool.query(
|
|
135
|
+
`WITH d AS (DELETE FROM ${this.table} WHERE updated_at < now() - ($1::double precision * interval '1 millisecond') RETURNING 1) SELECT count(*)::int AS n FROM d`,
|
|
136
|
+
[olderThanMs]
|
|
137
|
+
);
|
|
138
|
+
return Number(rows[0]?.n ?? 0);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
142
|
+
0 && (module.exports = {
|
|
143
|
+
PostgresStore,
|
|
144
|
+
postgresSchemaSql
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=postgres.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/postgres.ts"],"sourcesContent":["import type { ScopeState, Store, TransactResult } from '../types.js';\n\n/** The subset of `pg.PoolClient` replyonce uses. */\nexport interface PgClientLike {\n query(text: string, values?: unknown[]): Promise<{ rows: Array<Record<string, unknown>> }>;\n release(err?: Error | boolean): void;\n}\n\n/** The subset of `pg.Pool` replyonce uses. */\nexport interface PgPoolLike {\n connect(): Promise<PgClientLike>;\n query(text: string, values?: unknown[]): Promise<{ rows: Array<Record<string, unknown>> }>;\n}\n\nexport interface PostgresStoreOptions {\n pool: PgPoolLike;\n /** Table name, optionally schema-qualified. Default `replyonce_scopes`. */\n table?: string;\n}\n\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;\n\nfunction quoteTable(table: string): string {\n const parts = table.split('.');\n if (parts.length > 2 || !parts.every((p) => IDENT.test(p))) {\n throw new TypeError(`replyonce: invalid table name ${JSON.stringify(table)}`);\n }\n return parts.map((p) => `\"${p}\"`).join('.');\n}\n\n/** DDL for the store's table. Run it once (or call `migrate()`). */\nexport function postgresSchemaSql(table = 'replyonce_scopes'): string {\n const t = quoteTable(table);\n return [\n `CREATE TABLE IF NOT EXISTS ${t} (`,\n ` scope text PRIMARY KEY,`,\n ` state jsonb NOT NULL,`,\n ` version bigint NOT NULL DEFAULT 0,`,\n ` updated_at timestamptz NOT NULL DEFAULT now()`,\n `);`,\n ].join('\\n');\n}\n\n/**\n * Postgres store. Each operation is one transaction holding the scope row's lock\n * (`SELECT ... FOR UPDATE`), so concurrent workers on one scope are serialized and\n * workers on different scopes do not contend.\n */\nexport class PostgresStore implements Store {\n private readonly pool: PgPoolLike;\n private readonly table: string;\n readonly tableName: string;\n\n constructor(options: PostgresStoreOptions) {\n if (!options?.pool) throw new TypeError('replyonce: PostgresStore needs a pool');\n this.pool = options.pool;\n this.tableName = options.table ?? 'replyonce_scopes';\n this.table = quoteTable(this.tableName);\n }\n\n /**\n * Creates the table if it does not exist. Safe to call from several workers at once:\n * `CREATE TABLE IF NOT EXISTS` alone is not, because two sessions can both pass the\n * existence check and one then fails on the catalog's unique index, so concurrent\n * migrations of the same table are serialized with a transaction-scoped advisory lock.\n */\n async migrate(): Promise<void> {\n const client = await this.pool.connect();\n let broken: Error | undefined;\n try {\n await client.query('BEGIN');\n await client.query('SELECT pg_advisory_xact_lock(hashtext($1))', [`replyonce:migrate:${this.tableName}`]);\n await client.query(postgresSchemaSql(this.tableName));\n await client.query('COMMIT');\n } catch (err) {\n try {\n await client.query('ROLLBACK');\n } catch (rollbackErr) {\n broken = rollbackErr as Error;\n }\n throw err;\n } finally {\n client.release(broken);\n }\n }\n\n async read(scope: string): Promise<ScopeState | null> {\n const { rows } = await this.pool.query(`SELECT state FROM ${this.table} WHERE scope = $1`, [scope]);\n return (rows[0]?.state as ScopeState | undefined) ?? null;\n }\n\n async transact<T>(scope: string, fn: (state: ScopeState | null) => TransactResult<T>): Promise<T> {\n const client = await this.pool.connect();\n let broken: Error | undefined;\n try {\n await client.query('BEGIN');\n let { rows } = await client.query(`SELECT state FROM ${this.table} WHERE scope = $1 FOR UPDATE`, [scope]);\n let exists = rows.length > 0;\n const { next, result } = fn(exists ? ((rows[0]!.state as ScopeState) ?? null) : null);\n if (next) {\n if (!exists) {\n // Another transaction may insert the same scope first; if so, re-run under its lock.\n const ins = await client.query(\n `INSERT INTO ${this.table} (scope, state) VALUES ($1, $2::jsonb) ON CONFLICT (scope) DO NOTHING RETURNING scope`,\n [scope, JSON.stringify(next)],\n );\n if (ins.rows.length === 0) {\n ({ rows } = await client.query(`SELECT state FROM ${this.table} WHERE scope = $1 FOR UPDATE`, [scope]));\n exists = true;\n const again = fn((rows[0]?.state as ScopeState | undefined) ?? null);\n if (again.next) {\n await client.query(\n `UPDATE ${this.table} SET state = $2::jsonb, version = version + 1, updated_at = now() WHERE scope = $1`,\n [scope, JSON.stringify(again.next)],\n );\n }\n await client.query('COMMIT');\n return again.result;\n }\n } else {\n await client.query(\n `UPDATE ${this.table} SET state = $2::jsonb, version = version + 1, updated_at = now() WHERE scope = $1`,\n [scope, JSON.stringify(next)],\n );\n }\n }\n await client.query('COMMIT');\n return result;\n } catch (err) {\n try {\n await client.query('ROLLBACK');\n } catch (rollbackErr) {\n broken = rollbackErr instanceof Error ? rollbackErr : new Error(String(rollbackErr));\n }\n throw err;\n } finally {\n client.release(broken);\n }\n }\n\n /** Delete scopes untouched for longer than `olderThanMs`. Returns the number removed. */\n async purge(olderThanMs: number): Promise<number> {\n const { rows } = await this.pool.query(\n `WITH d AS (DELETE FROM ${this.table} WHERE updated_at < now() - ($1::double precision * interval '1 millisecond') RETURNING 1) SELECT count(*)::int AS n FROM d`,\n [olderThanMs],\n );\n return Number(rows[0]?.n ?? 0);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA,IAAM,QAAQ;AAEd,SAAS,WAAW,OAAuB;AACzC,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,SAAS,KAAK,CAAC,MAAM,MAAM,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC,GAAG;AAC1D,UAAM,IAAI,UAAU,iCAAiC,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,EAC9E;AACA,SAAO,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,GAAG;AAC5C;AAGO,SAAS,kBAAkB,QAAQ,oBAA4B;AACpE,QAAM,IAAI,WAAW,KAAK;AAC1B,SAAO;AAAA,IACL,8BAA8B,CAAC;AAAA,IAC/B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAOO,IAAM,gBAAN,MAAqC;AAAA,EACzB;AAAA,EACA;AAAA,EACR;AAAA,EAET,YAAY,SAA+B;AACzC,QAAI,CAAC,SAAS,KAAM,OAAM,IAAI,UAAU,uCAAuC;AAC/E,SAAK,OAAO,QAAQ;AACpB,SAAK,YAAY,QAAQ,SAAS;AAClC,SAAK,QAAQ,WAAW,KAAK,SAAS;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,UAAyB;AAC7B,UAAM,SAAS,MAAM,KAAK,KAAK,QAAQ;AACvC,QAAI;AACJ,QAAI;AACF,YAAM,OAAO,MAAM,OAAO;AAC1B,YAAM,OAAO,MAAM,8CAA8C,CAAC,qBAAqB,KAAK,SAAS,EAAE,CAAC;AACxG,YAAM,OAAO,MAAM,kBAAkB,KAAK,SAAS,CAAC;AACpD,YAAM,OAAO,MAAM,QAAQ;AAAA,IAC7B,SAAS,KAAK;AACZ,UAAI;AACF,cAAM,OAAO,MAAM,UAAU;AAAA,MAC/B,SAAS,aAAa;AACpB,iBAAS;AAAA,MACX;AACA,YAAM;AAAA,IACR,UAAE;AACA,aAAO,QAAQ,MAAM;AAAA,IACvB;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,OAA2C;AACpD,UAAM,EAAE,KAAK,IAAI,MAAM,KAAK,KAAK,MAAM,qBAAqB,KAAK,KAAK,qBAAqB,CAAC,KAAK,CAAC;AAClG,WAAQ,KAAK,CAAC,GAAG,SAAoC;AAAA,EACvD;AAAA,EAEA,MAAM,SAAY,OAAe,IAAiE;AAChG,UAAM,SAAS,MAAM,KAAK,KAAK,QAAQ;AACvC,QAAI;AACJ,QAAI;AACF,YAAM,OAAO,MAAM,OAAO;AAC1B,UAAI,EAAE,KAAK,IAAI,MAAM,OAAO,MAAM,qBAAqB,KAAK,KAAK,gCAAgC,CAAC,KAAK,CAAC;AACxG,UAAI,SAAS,KAAK,SAAS;AAC3B,YAAM,EAAE,MAAM,OAAO,IAAI,GAAG,SAAW,KAAK,CAAC,EAAG,SAAwB,OAAQ,IAAI;AACpF,UAAI,MAAM;AACR,YAAI,CAAC,QAAQ;AAEX,gBAAM,MAAM,MAAM,OAAO;AAAA,YACvB,eAAe,KAAK,KAAK;AAAA,YACzB,CAAC,OAAO,KAAK,UAAU,IAAI,CAAC;AAAA,UAC9B;AACA,cAAI,IAAI,KAAK,WAAW,GAAG;AACzB,aAAC,EAAE,KAAK,IAAI,MAAM,OAAO,MAAM,qBAAqB,KAAK,KAAK,gCAAgC,CAAC,KAAK,CAAC;AACrG,qBAAS;AACT,kBAAM,QAAQ,GAAI,KAAK,CAAC,GAAG,SAAoC,IAAI;AACnE,gBAAI,MAAM,MAAM;AACd,oBAAM,OAAO;AAAA,gBACX,UAAU,KAAK,KAAK;AAAA,gBACpB,CAAC,OAAO,KAAK,UAAU,MAAM,IAAI,CAAC;AAAA,cACpC;AAAA,YACF;AACA,kBAAM,OAAO,MAAM,QAAQ;AAC3B,mBAAO,MAAM;AAAA,UACf;AAAA,QACF,OAAO;AACL,gBAAM,OAAO;AAAA,YACX,UAAU,KAAK,KAAK;AAAA,YACpB,CAAC,OAAO,KAAK,UAAU,IAAI,CAAC;AAAA,UAC9B;AAAA,QACF;AAAA,MACF;AACA,YAAM,OAAO,MAAM,QAAQ;AAC3B,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,UAAI;AACF,cAAM,OAAO,MAAM,UAAU;AAAA,MAC/B,SAAS,aAAa;AACpB,iBAAS,uBAAuB,QAAQ,cAAc,IAAI,MAAM,OAAO,WAAW,CAAC;AAAA,MACrF;AACA,YAAM;AAAA,IACR,UAAE;AACA,aAAO,QAAQ,MAAM;AAAA,IACvB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,MAAM,aAAsC;AAChD,UAAM,EAAE,KAAK,IAAI,MAAM,KAAK,KAAK;AAAA,MAC/B,0BAA0B,KAAK,KAAK;AAAA,MACpC,CAAC,WAAW;AAAA,IACd;AACA,WAAO,OAAO,KAAK,CAAC,GAAG,KAAK,CAAC;AAAA,EAC/B;AACF;","names":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Store, ScopeState, TransactResult } from './index.cjs';
|
|
2
|
+
|
|
3
|
+
/** The subset of `pg.PoolClient` replyonce uses. */
|
|
4
|
+
interface PgClientLike {
|
|
5
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
6
|
+
rows: Array<Record<string, unknown>>;
|
|
7
|
+
}>;
|
|
8
|
+
release(err?: Error | boolean): void;
|
|
9
|
+
}
|
|
10
|
+
/** The subset of `pg.Pool` replyonce uses. */
|
|
11
|
+
interface PgPoolLike {
|
|
12
|
+
connect(): Promise<PgClientLike>;
|
|
13
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
14
|
+
rows: Array<Record<string, unknown>>;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
interface PostgresStoreOptions {
|
|
18
|
+
pool: PgPoolLike;
|
|
19
|
+
/** Table name, optionally schema-qualified. Default `replyonce_scopes`. */
|
|
20
|
+
table?: string;
|
|
21
|
+
}
|
|
22
|
+
/** DDL for the store's table. Run it once (or call `migrate()`). */
|
|
23
|
+
declare function postgresSchemaSql(table?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Postgres store. Each operation is one transaction holding the scope row's lock
|
|
26
|
+
* (`SELECT ... FOR UPDATE`), so concurrent workers on one scope are serialized and
|
|
27
|
+
* workers on different scopes do not contend.
|
|
28
|
+
*/
|
|
29
|
+
declare class PostgresStore implements Store {
|
|
30
|
+
private readonly pool;
|
|
31
|
+
private readonly table;
|
|
32
|
+
readonly tableName: string;
|
|
33
|
+
constructor(options: PostgresStoreOptions);
|
|
34
|
+
/**
|
|
35
|
+
* Creates the table if it does not exist. Safe to call from several workers at once:
|
|
36
|
+
* `CREATE TABLE IF NOT EXISTS` alone is not, because two sessions can both pass the
|
|
37
|
+
* existence check and one then fails on the catalog's unique index, so concurrent
|
|
38
|
+
* migrations of the same table are serialized with a transaction-scoped advisory lock.
|
|
39
|
+
*/
|
|
40
|
+
migrate(): Promise<void>;
|
|
41
|
+
read(scope: string): Promise<ScopeState | null>;
|
|
42
|
+
transact<T>(scope: string, fn: (state: ScopeState | null) => TransactResult<T>): Promise<T>;
|
|
43
|
+
/** Delete scopes untouched for longer than `olderThanMs`. Returns the number removed. */
|
|
44
|
+
purge(olderThanMs: number): Promise<number>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { PostgresStore, postgresSchemaSql };
|
|
48
|
+
export type { PgClientLike, PgPoolLike, PostgresStoreOptions };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Store, ScopeState, TransactResult } from './index.js';
|
|
2
|
+
|
|
3
|
+
/** The subset of `pg.PoolClient` replyonce uses. */
|
|
4
|
+
interface PgClientLike {
|
|
5
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
6
|
+
rows: Array<Record<string, unknown>>;
|
|
7
|
+
}>;
|
|
8
|
+
release(err?: Error | boolean): void;
|
|
9
|
+
}
|
|
10
|
+
/** The subset of `pg.Pool` replyonce uses. */
|
|
11
|
+
interface PgPoolLike {
|
|
12
|
+
connect(): Promise<PgClientLike>;
|
|
13
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
14
|
+
rows: Array<Record<string, unknown>>;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
interface PostgresStoreOptions {
|
|
18
|
+
pool: PgPoolLike;
|
|
19
|
+
/** Table name, optionally schema-qualified. Default `replyonce_scopes`. */
|
|
20
|
+
table?: string;
|
|
21
|
+
}
|
|
22
|
+
/** DDL for the store's table. Run it once (or call `migrate()`). */
|
|
23
|
+
declare function postgresSchemaSql(table?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Postgres store. Each operation is one transaction holding the scope row's lock
|
|
26
|
+
* (`SELECT ... FOR UPDATE`), so concurrent workers on one scope are serialized and
|
|
27
|
+
* workers on different scopes do not contend.
|
|
28
|
+
*/
|
|
29
|
+
declare class PostgresStore implements Store {
|
|
30
|
+
private readonly pool;
|
|
31
|
+
private readonly table;
|
|
32
|
+
readonly tableName: string;
|
|
33
|
+
constructor(options: PostgresStoreOptions);
|
|
34
|
+
/**
|
|
35
|
+
* Creates the table if it does not exist. Safe to call from several workers at once:
|
|
36
|
+
* `CREATE TABLE IF NOT EXISTS` alone is not, because two sessions can both pass the
|
|
37
|
+
* existence check and one then fails on the catalog's unique index, so concurrent
|
|
38
|
+
* migrations of the same table are serialized with a transaction-scoped advisory lock.
|
|
39
|
+
*/
|
|
40
|
+
migrate(): Promise<void>;
|
|
41
|
+
read(scope: string): Promise<ScopeState | null>;
|
|
42
|
+
transact<T>(scope: string, fn: (state: ScopeState | null) => TransactResult<T>): Promise<T>;
|
|
43
|
+
/** Delete scopes untouched for longer than `olderThanMs`. Returns the number removed. */
|
|
44
|
+
purge(olderThanMs: number): Promise<number>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { PostgresStore, postgresSchemaSql };
|
|
48
|
+
export type { PgClientLike, PgPoolLike, PostgresStoreOptions };
|
package/dist/postgres.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/stores/postgres.ts
|
|
2
|
+
var IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
3
|
+
function quoteTable(table) {
|
|
4
|
+
const parts = table.split(".");
|
|
5
|
+
if (parts.length > 2 || !parts.every((p) => IDENT.test(p))) {
|
|
6
|
+
throw new TypeError(`replyonce: invalid table name ${JSON.stringify(table)}`);
|
|
7
|
+
}
|
|
8
|
+
return parts.map((p) => `"${p}"`).join(".");
|
|
9
|
+
}
|
|
10
|
+
function postgresSchemaSql(table = "replyonce_scopes") {
|
|
11
|
+
const t = quoteTable(table);
|
|
12
|
+
return [
|
|
13
|
+
`CREATE TABLE IF NOT EXISTS ${t} (`,
|
|
14
|
+
` scope text PRIMARY KEY,`,
|
|
15
|
+
` state jsonb NOT NULL,`,
|
|
16
|
+
` version bigint NOT NULL DEFAULT 0,`,
|
|
17
|
+
` updated_at timestamptz NOT NULL DEFAULT now()`,
|
|
18
|
+
`);`
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
var PostgresStore = class {
|
|
22
|
+
pool;
|
|
23
|
+
table;
|
|
24
|
+
tableName;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
if (!options?.pool) throw new TypeError("replyonce: PostgresStore needs a pool");
|
|
27
|
+
this.pool = options.pool;
|
|
28
|
+
this.tableName = options.table ?? "replyonce_scopes";
|
|
29
|
+
this.table = quoteTable(this.tableName);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates the table if it does not exist. Safe to call from several workers at once:
|
|
33
|
+
* `CREATE TABLE IF NOT EXISTS` alone is not, because two sessions can both pass the
|
|
34
|
+
* existence check and one then fails on the catalog's unique index, so concurrent
|
|
35
|
+
* migrations of the same table are serialized with a transaction-scoped advisory lock.
|
|
36
|
+
*/
|
|
37
|
+
async migrate() {
|
|
38
|
+
const client = await this.pool.connect();
|
|
39
|
+
let broken;
|
|
40
|
+
try {
|
|
41
|
+
await client.query("BEGIN");
|
|
42
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`replyonce:migrate:${this.tableName}`]);
|
|
43
|
+
await client.query(postgresSchemaSql(this.tableName));
|
|
44
|
+
await client.query("COMMIT");
|
|
45
|
+
} catch (err) {
|
|
46
|
+
try {
|
|
47
|
+
await client.query("ROLLBACK");
|
|
48
|
+
} catch (rollbackErr) {
|
|
49
|
+
broken = rollbackErr;
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
} finally {
|
|
53
|
+
client.release(broken);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async read(scope) {
|
|
57
|
+
const { rows } = await this.pool.query(`SELECT state FROM ${this.table} WHERE scope = $1`, [scope]);
|
|
58
|
+
return rows[0]?.state ?? null;
|
|
59
|
+
}
|
|
60
|
+
async transact(scope, fn) {
|
|
61
|
+
const client = await this.pool.connect();
|
|
62
|
+
let broken;
|
|
63
|
+
try {
|
|
64
|
+
await client.query("BEGIN");
|
|
65
|
+
let { rows } = await client.query(`SELECT state FROM ${this.table} WHERE scope = $1 FOR UPDATE`, [scope]);
|
|
66
|
+
let exists = rows.length > 0;
|
|
67
|
+
const { next, result } = fn(exists ? rows[0].state ?? null : null);
|
|
68
|
+
if (next) {
|
|
69
|
+
if (!exists) {
|
|
70
|
+
const ins = await client.query(
|
|
71
|
+
`INSERT INTO ${this.table} (scope, state) VALUES ($1, $2::jsonb) ON CONFLICT (scope) DO NOTHING RETURNING scope`,
|
|
72
|
+
[scope, JSON.stringify(next)]
|
|
73
|
+
);
|
|
74
|
+
if (ins.rows.length === 0) {
|
|
75
|
+
({ rows } = await client.query(`SELECT state FROM ${this.table} WHERE scope = $1 FOR UPDATE`, [scope]));
|
|
76
|
+
exists = true;
|
|
77
|
+
const again = fn(rows[0]?.state ?? null);
|
|
78
|
+
if (again.next) {
|
|
79
|
+
await client.query(
|
|
80
|
+
`UPDATE ${this.table} SET state = $2::jsonb, version = version + 1, updated_at = now() WHERE scope = $1`,
|
|
81
|
+
[scope, JSON.stringify(again.next)]
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
await client.query("COMMIT");
|
|
85
|
+
return again.result;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
await client.query(
|
|
89
|
+
`UPDATE ${this.table} SET state = $2::jsonb, version = version + 1, updated_at = now() WHERE scope = $1`,
|
|
90
|
+
[scope, JSON.stringify(next)]
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await client.query("COMMIT");
|
|
95
|
+
return result;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
try {
|
|
98
|
+
await client.query("ROLLBACK");
|
|
99
|
+
} catch (rollbackErr) {
|
|
100
|
+
broken = rollbackErr instanceof Error ? rollbackErr : new Error(String(rollbackErr));
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
} finally {
|
|
104
|
+
client.release(broken);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Delete scopes untouched for longer than `olderThanMs`. Returns the number removed. */
|
|
108
|
+
async purge(olderThanMs) {
|
|
109
|
+
const { rows } = await this.pool.query(
|
|
110
|
+
`WITH d AS (DELETE FROM ${this.table} WHERE updated_at < now() - ($1::double precision * interval '1 millisecond') RETURNING 1) SELECT count(*)::int AS n FROM d`,
|
|
111
|
+
[olderThanMs]
|
|
112
|
+
);
|
|
113
|
+
return Number(rows[0]?.n ?? 0);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
export {
|
|
117
|
+
PostgresStore,
|
|
118
|
+
postgresSchemaSql
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/postgres.ts"],"sourcesContent":["import type { ScopeState, Store, TransactResult } from '../types.js';\n\n/** The subset of `pg.PoolClient` replyonce uses. */\nexport interface PgClientLike {\n query(text: string, values?: unknown[]): Promise<{ rows: Array<Record<string, unknown>> }>;\n release(err?: Error | boolean): void;\n}\n\n/** The subset of `pg.Pool` replyonce uses. */\nexport interface PgPoolLike {\n connect(): Promise<PgClientLike>;\n query(text: string, values?: unknown[]): Promise<{ rows: Array<Record<string, unknown>> }>;\n}\n\nexport interface PostgresStoreOptions {\n pool: PgPoolLike;\n /** Table name, optionally schema-qualified. Default `replyonce_scopes`. */\n table?: string;\n}\n\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;\n\nfunction quoteTable(table: string): string {\n const parts = table.split('.');\n if (parts.length > 2 || !parts.every((p) => IDENT.test(p))) {\n throw new TypeError(`replyonce: invalid table name ${JSON.stringify(table)}`);\n }\n return parts.map((p) => `\"${p}\"`).join('.');\n}\n\n/** DDL for the store's table. Run it once (or call `migrate()`). */\nexport function postgresSchemaSql(table = 'replyonce_scopes'): string {\n const t = quoteTable(table);\n return [\n `CREATE TABLE IF NOT EXISTS ${t} (`,\n ` scope text PRIMARY KEY,`,\n ` state jsonb NOT NULL,`,\n ` version bigint NOT NULL DEFAULT 0,`,\n ` updated_at timestamptz NOT NULL DEFAULT now()`,\n `);`,\n ].join('\\n');\n}\n\n/**\n * Postgres store. Each operation is one transaction holding the scope row's lock\n * (`SELECT ... FOR UPDATE`), so concurrent workers on one scope are serialized and\n * workers on different scopes do not contend.\n */\nexport class PostgresStore implements Store {\n private readonly pool: PgPoolLike;\n private readonly table: string;\n readonly tableName: string;\n\n constructor(options: PostgresStoreOptions) {\n if (!options?.pool) throw new TypeError('replyonce: PostgresStore needs a pool');\n this.pool = options.pool;\n this.tableName = options.table ?? 'replyonce_scopes';\n this.table = quoteTable(this.tableName);\n }\n\n /**\n * Creates the table if it does not exist. Safe to call from several workers at once:\n * `CREATE TABLE IF NOT EXISTS` alone is not, because two sessions can both pass the\n * existence check and one then fails on the catalog's unique index, so concurrent\n * migrations of the same table are serialized with a transaction-scoped advisory lock.\n */\n async migrate(): Promise<void> {\n const client = await this.pool.connect();\n let broken: Error | undefined;\n try {\n await client.query('BEGIN');\n await client.query('SELECT pg_advisory_xact_lock(hashtext($1))', [`replyonce:migrate:${this.tableName}`]);\n await client.query(postgresSchemaSql(this.tableName));\n await client.query('COMMIT');\n } catch (err) {\n try {\n await client.query('ROLLBACK');\n } catch (rollbackErr) {\n broken = rollbackErr as Error;\n }\n throw err;\n } finally {\n client.release(broken);\n }\n }\n\n async read(scope: string): Promise<ScopeState | null> {\n const { rows } = await this.pool.query(`SELECT state FROM ${this.table} WHERE scope = $1`, [scope]);\n return (rows[0]?.state as ScopeState | undefined) ?? null;\n }\n\n async transact<T>(scope: string, fn: (state: ScopeState | null) => TransactResult<T>): Promise<T> {\n const client = await this.pool.connect();\n let broken: Error | undefined;\n try {\n await client.query('BEGIN');\n let { rows } = await client.query(`SELECT state FROM ${this.table} WHERE scope = $1 FOR UPDATE`, [scope]);\n let exists = rows.length > 0;\n const { next, result } = fn(exists ? ((rows[0]!.state as ScopeState) ?? null) : null);\n if (next) {\n if (!exists) {\n // Another transaction may insert the same scope first; if so, re-run under its lock.\n const ins = await client.query(\n `INSERT INTO ${this.table} (scope, state) VALUES ($1, $2::jsonb) ON CONFLICT (scope) DO NOTHING RETURNING scope`,\n [scope, JSON.stringify(next)],\n );\n if (ins.rows.length === 0) {\n ({ rows } = await client.query(`SELECT state FROM ${this.table} WHERE scope = $1 FOR UPDATE`, [scope]));\n exists = true;\n const again = fn((rows[0]?.state as ScopeState | undefined) ?? null);\n if (again.next) {\n await client.query(\n `UPDATE ${this.table} SET state = $2::jsonb, version = version + 1, updated_at = now() WHERE scope = $1`,\n [scope, JSON.stringify(again.next)],\n );\n }\n await client.query('COMMIT');\n return again.result;\n }\n } else {\n await client.query(\n `UPDATE ${this.table} SET state = $2::jsonb, version = version + 1, updated_at = now() WHERE scope = $1`,\n [scope, JSON.stringify(next)],\n );\n }\n }\n await client.query('COMMIT');\n return result;\n } catch (err) {\n try {\n await client.query('ROLLBACK');\n } catch (rollbackErr) {\n broken = rollbackErr instanceof Error ? rollbackErr : new Error(String(rollbackErr));\n }\n throw err;\n } finally {\n client.release(broken);\n }\n }\n\n /** Delete scopes untouched for longer than `olderThanMs`. Returns the number removed. */\n async purge(olderThanMs: number): Promise<number> {\n const { rows } = await this.pool.query(\n `WITH d AS (DELETE FROM ${this.table} WHERE updated_at < now() - ($1::double precision * interval '1 millisecond') RETURNING 1) SELECT count(*)::int AS n FROM d`,\n [olderThanMs],\n );\n return Number(rows[0]?.n ?? 0);\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/redis.cjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/stores/redis.ts
|
|
21
|
+
var redis_exports = {};
|
|
22
|
+
__export(redis_exports, {
|
|
23
|
+
RedisStore: () => RedisStore,
|
|
24
|
+
StoreContentionError: () => StoreContentionError
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(redis_exports);
|
|
27
|
+
var READ = `return redis.call('HMGET', KEYS[1], 's', 'v')`;
|
|
28
|
+
var CAS = `
|
|
29
|
+
local v = tonumber(redis.call('HGET', KEYS[1], 'v') or '0')
|
|
30
|
+
if v ~= tonumber(ARGV[1]) then return 0 end
|
|
31
|
+
redis.call('HSET', KEYS[1], 's', ARGV[2], 'v', v + 1)
|
|
32
|
+
local ttl = tonumber(ARGV[3])
|
|
33
|
+
if ttl > 0 then redis.call('PEXPIRE', KEYS[1], ttl) end
|
|
34
|
+
return 1
|
|
35
|
+
`;
|
|
36
|
+
var StoreContentionError = class extends Error {
|
|
37
|
+
constructor(scope, attempts) {
|
|
38
|
+
super(`replyonce: gave up on scope ${JSON.stringify(scope)} after ${attempts} contended attempts`);
|
|
39
|
+
this.name = "StoreContentionError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var RedisStore = class {
|
|
43
|
+
client;
|
|
44
|
+
prefix;
|
|
45
|
+
keyTtlMs;
|
|
46
|
+
maxRetries;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
if (!options?.client) throw new TypeError("replyonce: RedisStore needs a client");
|
|
49
|
+
this.client = options.client;
|
|
50
|
+
this.prefix = options.prefix ?? "replyonce";
|
|
51
|
+
this.keyTtlMs = options.keyTtlMs ?? 30 * 24 * 36e5;
|
|
52
|
+
this.maxRetries = options.maxRetries ?? 200;
|
|
53
|
+
}
|
|
54
|
+
redisKey(scope) {
|
|
55
|
+
return `${this.prefix}:{${scope}}`;
|
|
56
|
+
}
|
|
57
|
+
async load(scope) {
|
|
58
|
+
const reply = await this.client.eval(READ, 1, this.redisKey(scope));
|
|
59
|
+
const raw = reply?.[0] ?? null;
|
|
60
|
+
const version = reply?.[1] ? Number(reply[1]) : 0;
|
|
61
|
+
return { state: raw ? JSON.parse(raw) : null, version };
|
|
62
|
+
}
|
|
63
|
+
async read(scope) {
|
|
64
|
+
return (await this.load(scope)).state;
|
|
65
|
+
}
|
|
66
|
+
async transact(scope, fn) {
|
|
67
|
+
const key = this.redisKey(scope);
|
|
68
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
69
|
+
const { state, version } = await this.load(scope);
|
|
70
|
+
const { next, result } = fn(state);
|
|
71
|
+
if (!next) return result;
|
|
72
|
+
const ok = await this.client.eval(CAS, 1, key, version, JSON.stringify(next), this.keyTtlMs);
|
|
73
|
+
if (Number(ok) === 1) return result;
|
|
74
|
+
const cap = Math.min(50, 2 ** Math.min(attempt, 6));
|
|
75
|
+
await new Promise((r) => setTimeout(r, Math.random() * cap));
|
|
76
|
+
}
|
|
77
|
+
throw new StoreContentionError(scope, this.maxRetries + 1);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
81
|
+
0 && (module.exports = {
|
|
82
|
+
RedisStore,
|
|
83
|
+
StoreContentionError
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=redis.cjs.map
|