@titan-design/hitl 0.1.2 → 0.2.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/README.md CHANGED
@@ -4,8 +4,20 @@ Pause work on a human, and let a different process resume it. The primitive is
4
4
  "the same work, paused": `openGate()` from inside a task hands back something to
5
5
  await, and `resolveGate()` from anywhere else lets it continue.
6
6
 
7
- Tier 1 of the titan-platform DAG (TP-11). Depends on
8
- `@titan-design/store-sqlite`; `zod` is a peer (v4).
7
+ Tier 1 of the titan-platform DAG (TP-11). `zod` is a peer (v4).
8
+
9
+ ## Two entries
10
+
11
+ The root entry (`@titan-design/hitl`) is runtime-neutral: the gate state machine,
12
+ `GateStore`, and `MemoryGateStore`, with no `node:*` import and no native addon, so it
13
+ loads in a Cloudflare Workers isolate. `SqliteGateStore` and its migration helpers moved to
14
+ a subpath, `@titan-design/hitl/sqlite`, which is the only part of the package that pulls in
15
+ `better-sqlite3` (via `@titan-design/store-sqlite`).
16
+
17
+ **Migrating from before 0.2.0:** change `import { SqliteGateStore, gateMigration } from
18
+ "@titan-design/hitl"` to `import { SqliteGateStore, gateMigration } from
19
+ "@titan-design/hitl/sqlite"`. Everything else (`openGate`, `resolveGate`, `GateStore`,
20
+ `MemoryGateStore`, the error classes) still comes from the root.
9
21
 
10
22
  ## A gate is a row, not a promise
11
23
 
@@ -18,7 +30,8 @@ the same SQLite file.
18
30
 
19
31
  ```ts
20
32
  import { openDatabase } from "@titan-design/store-sqlite";
21
- import { SqliteGateStore, openGate } from "@titan-design/hitl";
33
+ import { SqliteGateStore } from "@titan-design/hitl/sqlite";
34
+ import { openGate } from "@titan-design/hitl";
22
35
  import { z } from "zod";
23
36
 
24
37
  const store = new SqliteGateStore(openDatabase("~/.local/state/thing/gates.sqlite3"));
@@ -0,0 +1,87 @@
1
+ /** A gate is a row, never a promise: pending work survives the process that opened it. */
2
+ type GateStatus = "pending" | "resolved" | "cancelled" | "expired";
3
+ /** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */
4
+ type JsonSchema = Record<string, unknown>;
5
+ interface GateRecord {
6
+ id: string;
7
+ prompt: string;
8
+ schema: JsonSchema | undefined;
9
+ status: GateStatus;
10
+ payload: unknown;
11
+ /** Why the gate was cancelled; unset for every other status. */
12
+ reason: string | undefined;
13
+ /** ISO-8601 with milliseconds, the shape store-sqlite writes and any surface can send as-is. */
14
+ createdAt: string;
15
+ resolvedAt: string | undefined;
16
+ expiresAt: string | undefined;
17
+ }
18
+ interface GateInput {
19
+ /** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */
20
+ id?: string;
21
+ prompt: string;
22
+ schema?: JsonSchema;
23
+ expiresAt?: Date | string;
24
+ }
25
+ /**
26
+ * Synchronous on purpose: both implementations are local (a Map, and
27
+ * better-sqlite3), and `wait` is the only thing that needs to be async.
28
+ */
29
+ interface GateStore {
30
+ create(input: GateInput): GateRecord;
31
+ get(id: string): GateRecord | undefined;
32
+ resolve(id: string, payload: unknown): GateRecord;
33
+ cancel(id: string, reason: string): GateRecord;
34
+ listPending(): GateRecord[];
35
+ }
36
+ declare class GateError extends Error {
37
+ readonly gateId: string;
38
+ constructor(message: string, gateId: string);
39
+ }
40
+ declare class GateNotFound extends GateError {
41
+ constructor(gateId: string);
42
+ }
43
+ declare class GateAlreadyExists extends GateError {
44
+ constructor(gateId: string);
45
+ }
46
+ declare class GateAlreadySettled extends GateError {
47
+ readonly status: GateStatus;
48
+ constructor(gateId: string, status: GateStatus);
49
+ }
50
+ declare class GateCancelled extends GateError {
51
+ readonly reason: string;
52
+ constructor(gateId: string, reason: string);
53
+ }
54
+ declare class GateExpired extends GateError {
55
+ constructor(gateId: string);
56
+ }
57
+ declare class GatePayloadInvalid extends GateError {
58
+ readonly issues: string[];
59
+ constructor(gateId: string, issues: string[]);
60
+ }
61
+ declare class GateAborted extends GateError {
62
+ constructor(gateId: string, reason: string);
63
+ }
64
+
65
+ /**
66
+ * Every settle rule lives here so the memory and SQLite stores cannot drift
67
+ * apart. Subclasses only move rows.
68
+ */
69
+ declare abstract class BaseGateStore implements GateStore {
70
+ protected readonly clock: () => number;
71
+ protected constructor(clock: () => number);
72
+ protected abstract insert(record: GateRecord): void;
73
+ protected abstract read(id: string): GateRecord | undefined;
74
+ protected abstract update(record: GateRecord): void;
75
+ protected abstract readByStatus(status: GateRecord["status"]): GateRecord[];
76
+ create(input: GateInput): GateRecord;
77
+ get(id: string): GateRecord | undefined;
78
+ resolve(id: string, payload: unknown): GateRecord;
79
+ cancel(id: string, reason: string): GateRecord;
80
+ listPending(): GateRecord[];
81
+ private requirePending;
82
+ /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
83
+ private lapseIfExpired;
84
+ protected nowIso(): string;
85
+ }
86
+
87
+ export { BaseGateStore as B, type GateRecord as G, type JsonSchema as J, type GateStore as a, GateAborted as b, GateAlreadyExists as c, GateAlreadySettled as d, GateCancelled as e, GateError as f, GateExpired as g, type GateInput as h, GateNotFound as i, GatePayloadInvalid as j, type GateStatus as k };
@@ -0,0 +1,205 @@
1
+ // src/types.ts
2
+ var GateError = class extends Error {
3
+ constructor(message, gateId) {
4
+ super(message);
5
+ this.gateId = gateId;
6
+ this.name = new.target.name;
7
+ }
8
+ gateId;
9
+ };
10
+ var GateNotFound = class extends GateError {
11
+ constructor(gateId) {
12
+ super(`no gate with id ${gateId}`, gateId);
13
+ }
14
+ };
15
+ var GateAlreadyExists = class extends GateError {
16
+ constructor(gateId) {
17
+ super(`a gate with id ${gateId} already exists`, gateId);
18
+ }
19
+ };
20
+ var GateAlreadySettled = class extends GateError {
21
+ constructor(gateId, status) {
22
+ super(`gate ${gateId} is already ${status}`, gateId);
23
+ this.status = status;
24
+ }
25
+ status;
26
+ };
27
+ var GateCancelled = class extends GateError {
28
+ constructor(gateId, reason) {
29
+ super(`gate ${gateId} was cancelled: ${reason}`, gateId);
30
+ this.reason = reason;
31
+ }
32
+ reason;
33
+ };
34
+ var GateExpired = class extends GateError {
35
+ constructor(gateId) {
36
+ super(`gate ${gateId} expired before anyone resolved it`, gateId);
37
+ }
38
+ };
39
+ var GatePayloadInvalid = class extends GateError {
40
+ constructor(gateId, issues) {
41
+ super(`payload for gate ${gateId} does not match its schema: ${issues.join("; ")}`, gateId);
42
+ this.issues = issues;
43
+ }
44
+ issues;
45
+ };
46
+ var GateAborted = class extends GateError {
47
+ constructor(gateId, reason) {
48
+ super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);
49
+ }
50
+ };
51
+
52
+ // src/json-schema.ts
53
+ function checkAgainstJsonSchema(schema, value, path = "$") {
54
+ const issues = [];
55
+ collect(schema, value, path, issues);
56
+ return issues;
57
+ }
58
+ function collect(schema, value, path, issues) {
59
+ if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {
60
+ checkUnion(schema.anyOf ?? schema.oneOf, value, path, issues);
61
+ return;
62
+ }
63
+ if ("const" in schema && !Object.is(schema.const, value)) {
64
+ issues.push(`${path}: expected ${JSON.stringify(schema.const)}`);
65
+ return;
66
+ }
67
+ if (Array.isArray(schema.enum) && !schema.enum.some((option) => Object.is(option, value))) {
68
+ issues.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
69
+ return;
70
+ }
71
+ if (typeof schema.type !== "string") return;
72
+ if (!matchesType(schema.type, value)) {
73
+ issues.push(`${path}: expected ${schema.type}, received ${describe(value)}`);
74
+ return;
75
+ }
76
+ if (schema.type === "object") checkObject(schema, value, path, issues);
77
+ if (schema.type === "array") checkArray(schema, value, path, issues);
78
+ }
79
+ function checkUnion(branches, value, path, issues) {
80
+ const matched = branches.some((branch) => checkAgainstJsonSchema(branch, value, path).length === 0);
81
+ if (!matched) issues.push(`${path}: matched none of the ${branches.length} allowed shapes`);
82
+ }
83
+ function checkObject(schema, value, path, issues) {
84
+ const properties = schema.properties ?? {};
85
+ for (const key of schema.required ?? []) {
86
+ if (!(key in value)) issues.push(`${path}.${key}: required`);
87
+ }
88
+ if (schema.additionalProperties === false) {
89
+ for (const key of Object.keys(value)) {
90
+ if (!(key in properties)) issues.push(`${path}.${key}: unexpected property`);
91
+ }
92
+ }
93
+ for (const [key, sub] of Object.entries(properties)) {
94
+ if (key in value) collect(sub, value[key], `${path}.${key}`, issues);
95
+ }
96
+ }
97
+ function checkArray(schema, value, path, issues) {
98
+ const items = schema.items;
99
+ if (!items) return;
100
+ value.forEach((entry, index) => collect(items, entry, `${path}[${index}]`, issues));
101
+ }
102
+ function matchesType(type, value) {
103
+ switch (type) {
104
+ case "object":
105
+ return typeof value === "object" && value !== null && !Array.isArray(value);
106
+ case "array":
107
+ return Array.isArray(value);
108
+ case "integer":
109
+ return Number.isInteger(value);
110
+ case "number":
111
+ return typeof value === "number" && Number.isFinite(value);
112
+ case "null":
113
+ return value === null;
114
+ default:
115
+ return typeof value === type;
116
+ }
117
+ }
118
+ function describe(value) {
119
+ if (value === null) return "null";
120
+ return Array.isArray(value) ? "array" : typeof value;
121
+ }
122
+
123
+ // src/base-store.ts
124
+ var BaseGateStore = class {
125
+ constructor(clock) {
126
+ this.clock = clock;
127
+ }
128
+ clock;
129
+ create(input) {
130
+ const id = input.id ?? globalThis.crypto.randomUUID();
131
+ if (this.read(id)) throw new GateAlreadyExists(id);
132
+ const record = {
133
+ id,
134
+ prompt: input.prompt,
135
+ schema: input.schema,
136
+ status: "pending",
137
+ payload: void 0,
138
+ reason: void 0,
139
+ createdAt: this.nowIso(),
140
+ resolvedAt: void 0,
141
+ expiresAt: toIso(input.expiresAt)
142
+ };
143
+ this.insert(record);
144
+ return record;
145
+ }
146
+ get(id) {
147
+ const record = this.read(id);
148
+ return record ? this.lapseIfExpired(record) : void 0;
149
+ }
150
+ resolve(id, payload) {
151
+ const record = this.requirePending(id);
152
+ if (record.schema) {
153
+ const issues = checkAgainstJsonSchema(record.schema, payload);
154
+ if (issues.length > 0) throw new GatePayloadInvalid(id, issues);
155
+ }
156
+ const resolved = { ...record, status: "resolved", payload, resolvedAt: this.nowIso() };
157
+ this.update(resolved);
158
+ return resolved;
159
+ }
160
+ cancel(id, reason) {
161
+ const record = this.requirePending(id);
162
+ const cancelled = { ...record, status: "cancelled", reason, resolvedAt: this.nowIso() };
163
+ this.update(cancelled);
164
+ return cancelled;
165
+ }
166
+ listPending() {
167
+ return this.readByStatus("pending").map((record) => this.lapseIfExpired(record)).filter((record) => record.status === "pending").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
168
+ }
169
+ requirePending(id) {
170
+ const record = this.get(id);
171
+ if (!record) throw new GateNotFound(id);
172
+ if (record.status === "expired") throw new GateExpired(id);
173
+ if (record.status !== "pending") throw new GateAlreadySettled(id, record.status);
174
+ return record;
175
+ }
176
+ /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
177
+ lapseIfExpired(record) {
178
+ if (record.status !== "pending" || !record.expiresAt) return record;
179
+ if (Date.parse(record.expiresAt) > this.clock()) return record;
180
+ const expired = { ...record, status: "expired", resolvedAt: this.nowIso() };
181
+ this.update(expired);
182
+ return expired;
183
+ }
184
+ nowIso() {
185
+ return new Date(this.clock()).toISOString();
186
+ }
187
+ };
188
+ function toIso(value) {
189
+ if (value === void 0) return void 0;
190
+ return typeof value === "string" ? value : value.toISOString();
191
+ }
192
+
193
+ export {
194
+ GateError,
195
+ GateNotFound,
196
+ GateAlreadyExists,
197
+ GateAlreadySettled,
198
+ GateCancelled,
199
+ GateExpired,
200
+ GatePayloadInvalid,
201
+ GateAborted,
202
+ checkAgainstJsonSchema,
203
+ BaseGateStore
204
+ };
205
+ //# sourceMappingURL=chunk-5P4RFTKR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/json-schema.ts","../src/base-store.ts"],"sourcesContent":["/** A gate is a row, never a promise: pending work survives the process that opened it. */\nexport type GateStatus = \"pending\" | \"resolved\" | \"cancelled\" | \"expired\";\n\n/** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */\nexport type JsonSchema = Record<string, unknown>;\n\nexport interface GateRecord {\n id: string;\n prompt: string;\n schema: JsonSchema | undefined;\n status: GateStatus;\n payload: unknown;\n /** Why the gate was cancelled; unset for every other status. */\n reason: string | undefined;\n /** ISO-8601 with milliseconds, the shape store-sqlite writes and any surface can send as-is. */\n createdAt: string;\n resolvedAt: string | undefined;\n expiresAt: string | undefined;\n}\n\nexport interface GateInput {\n /** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */\n id?: string;\n prompt: string;\n schema?: JsonSchema;\n expiresAt?: Date | string;\n}\n\n/**\n * Synchronous on purpose: both implementations are local (a Map, and\n * better-sqlite3), and `wait` is the only thing that needs to be async.\n */\nexport interface GateStore {\n create(input: GateInput): GateRecord;\n get(id: string): GateRecord | undefined;\n resolve(id: string, payload: unknown): GateRecord;\n cancel(id: string, reason: string): GateRecord;\n listPending(): GateRecord[];\n}\n\nexport class GateError extends Error {\n constructor(\n message: string,\n readonly gateId: string,\n ) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class GateNotFound extends GateError {\n constructor(gateId: string) {\n super(`no gate with id ${gateId}`, gateId);\n }\n}\n\nexport class GateAlreadyExists extends GateError {\n constructor(gateId: string) {\n super(`a gate with id ${gateId} already exists`, gateId);\n }\n}\n\nexport class GateAlreadySettled extends GateError {\n constructor(\n gateId: string,\n readonly status: GateStatus,\n ) {\n super(`gate ${gateId} is already ${status}`, gateId);\n }\n}\n\nexport class GateCancelled extends GateError {\n constructor(\n gateId: string,\n readonly reason: string,\n ) {\n super(`gate ${gateId} was cancelled: ${reason}`, gateId);\n }\n}\n\nexport class GateExpired extends GateError {\n constructor(gateId: string) {\n super(`gate ${gateId} expired before anyone resolved it`, gateId);\n }\n}\n\nexport class GatePayloadInvalid extends GateError {\n constructor(\n gateId: string,\n readonly issues: string[],\n ) {\n super(`payload for gate ${gateId} does not match its schema: ${issues.join(\"; \")}`, gateId);\n }\n}\n\nexport class GateAborted extends GateError {\n constructor(gateId: string, reason: string) {\n super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);\n }\n}\n","import type { JsonSchema } from \"./types.js\";\n\n/**\n * A deliberately small structural check over the JSON Schema subset `z.toJSONSchema`\n * emits. It exists so the process that resolves a gate can reject a bad payload\n * without holding the original zod schema, which by definition lives in another\n * process. The waiter still re-validates with the real zod schema, so this is the\n * boundary check, not the type guarantee.\n */\nexport function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path = \"$\"): string[] {\n const issues: string[] = [];\n collect(schema, value, path, issues);\n return issues;\n}\n\nfunction collect(schema: JsonSchema, value: unknown, path: string, issues: string[]): void {\n if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {\n checkUnion((schema.anyOf ?? schema.oneOf) as JsonSchema[], value, path, issues);\n return;\n }\n if (\"const\" in schema && !Object.is(schema.const, value)) {\n issues.push(`${path}: expected ${JSON.stringify(schema.const)}`);\n return;\n }\n if (Array.isArray(schema.enum) && !schema.enum.some((option) => Object.is(option, value))) {\n issues.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);\n return;\n }\n if (typeof schema.type !== \"string\") return;\n if (!matchesType(schema.type, value)) {\n issues.push(`${path}: expected ${schema.type}, received ${describe(value)}`);\n return;\n }\n if (schema.type === \"object\") checkObject(schema, value as Record<string, unknown>, path, issues);\n if (schema.type === \"array\") checkArray(schema, value as unknown[], path, issues);\n}\n\nfunction checkUnion(branches: JsonSchema[], value: unknown, path: string, issues: string[]): void {\n const matched = branches.some((branch) => checkAgainstJsonSchema(branch, value, path).length === 0);\n if (!matched) issues.push(`${path}: matched none of the ${branches.length} allowed shapes`);\n}\n\nfunction checkObject(schema: JsonSchema, value: Record<string, unknown>, path: string, issues: string[]): void {\n const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;\n for (const key of (schema.required ?? []) as string[]) {\n if (!(key in value)) issues.push(`${path}.${key}: required`);\n }\n if (schema.additionalProperties === false) {\n for (const key of Object.keys(value)) {\n if (!(key in properties)) issues.push(`${path}.${key}: unexpected property`);\n }\n }\n for (const [key, sub] of Object.entries(properties)) {\n if (key in value) collect(sub, value[key], `${path}.${key}`, issues);\n }\n}\n\nfunction checkArray(schema: JsonSchema, value: unknown[], path: string, issues: string[]): void {\n const items = schema.items as JsonSchema | undefined;\n if (!items) return;\n value.forEach((entry, index) => collect(items, entry, `${path}[${index}]`, issues));\n}\n\nfunction matchesType(type: string, value: unknown): boolean {\n switch (type) {\n case \"object\":\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n case \"array\":\n return Array.isArray(value);\n case \"integer\":\n return Number.isInteger(value);\n case \"number\":\n return typeof value === \"number\" && Number.isFinite(value);\n case \"null\":\n return value === null;\n default:\n return typeof value === type;\n }\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n return Array.isArray(value) ? \"array\" : typeof value;\n}\n","import { checkAgainstJsonSchema } from \"./json-schema.js\";\nimport {\n GateAlreadyExists,\n GateAlreadySettled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateInput,\n type GateRecord,\n type GateStore,\n} from \"./types.js\";\n\n/**\n * Every settle rule lives here so the memory and SQLite stores cannot drift\n * apart. Subclasses only move rows.\n */\nexport abstract class BaseGateStore implements GateStore {\n protected constructor(protected readonly clock: () => number) {}\n\n protected abstract insert(record: GateRecord): void;\n protected abstract read(id: string): GateRecord | undefined;\n protected abstract update(record: GateRecord): void;\n protected abstract readByStatus(status: GateRecord[\"status\"]): GateRecord[];\n\n create(input: GateInput): GateRecord {\n const id = input.id ?? globalThis.crypto.randomUUID();\n if (this.read(id)) throw new GateAlreadyExists(id);\n const record: GateRecord = {\n id,\n prompt: input.prompt,\n schema: input.schema,\n status: \"pending\",\n payload: undefined,\n reason: undefined,\n createdAt: this.nowIso(),\n resolvedAt: undefined,\n expiresAt: toIso(input.expiresAt),\n };\n this.insert(record);\n return record;\n }\n\n get(id: string): GateRecord | undefined {\n const record = this.read(id);\n return record ? this.lapseIfExpired(record) : undefined;\n }\n\n resolve(id: string, payload: unknown): GateRecord {\n const record = this.requirePending(id);\n if (record.schema) {\n const issues = checkAgainstJsonSchema(record.schema, payload);\n if (issues.length > 0) throw new GatePayloadInvalid(id, issues);\n }\n const resolved: GateRecord = { ...record, status: \"resolved\", payload, resolvedAt: this.nowIso() };\n this.update(resolved);\n return resolved;\n }\n\n cancel(id: string, reason: string): GateRecord {\n const record = this.requirePending(id);\n const cancelled: GateRecord = { ...record, status: \"cancelled\", reason, resolvedAt: this.nowIso() };\n this.update(cancelled);\n return cancelled;\n }\n\n listPending(): GateRecord[] {\n return this.readByStatus(\"pending\")\n .map((record) => this.lapseIfExpired(record))\n .filter((record) => record.status === \"pending\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }\n\n private requirePending(id: string): GateRecord {\n const record = this.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"expired\") throw new GateExpired(id);\n if (record.status !== \"pending\") throw new GateAlreadySettled(id, record.status);\n return record;\n }\n\n /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */\n private lapseIfExpired(record: GateRecord): GateRecord {\n if (record.status !== \"pending\" || !record.expiresAt) return record;\n if (Date.parse(record.expiresAt) > this.clock()) return record;\n const expired: GateRecord = { ...record, status: \"expired\", resolvedAt: this.nowIso() };\n this.update(expired);\n return expired;\n }\n\n protected nowIso(): string {\n return new Date(this.clock()).toISOString();\n }\n}\n\nfunction toIso(value: Date | string | undefined): string | undefined {\n if (value === undefined) return undefined;\n return typeof value === \"string\" ? value : value.toISOString();\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,69 +1,6 @@
1
1
  import { ZodType } from 'zod';
2
- import { Db, Migration } from '@titan-design/store-sqlite';
3
-
4
- /** A gate is a row, never a promise: pending work survives the process that opened it. */
5
- type GateStatus = "pending" | "resolved" | "cancelled" | "expired";
6
- /** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */
7
- type JsonSchema = Record<string, unknown>;
8
- interface GateRecord {
9
- id: string;
10
- prompt: string;
11
- schema: JsonSchema | undefined;
12
- status: GateStatus;
13
- payload: unknown;
14
- /** Why the gate was cancelled; unset for every other status. */
15
- reason: string | undefined;
16
- /** ISO-8601 with milliseconds, the shape store-sqlite writes and any surface can send as-is. */
17
- createdAt: string;
18
- resolvedAt: string | undefined;
19
- expiresAt: string | undefined;
20
- }
21
- interface GateInput {
22
- /** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */
23
- id?: string;
24
- prompt: string;
25
- schema?: JsonSchema;
26
- expiresAt?: Date | string;
27
- }
28
- /**
29
- * Synchronous on purpose: both implementations are local (a Map, and
30
- * better-sqlite3), and `wait` is the only thing that needs to be async.
31
- */
32
- interface GateStore {
33
- create(input: GateInput): GateRecord;
34
- get(id: string): GateRecord | undefined;
35
- resolve(id: string, payload: unknown): GateRecord;
36
- cancel(id: string, reason: string): GateRecord;
37
- listPending(): GateRecord[];
38
- }
39
- declare class GateError extends Error {
40
- readonly gateId: string;
41
- constructor(message: string, gateId: string);
42
- }
43
- declare class GateNotFound extends GateError {
44
- constructor(gateId: string);
45
- }
46
- declare class GateAlreadyExists extends GateError {
47
- constructor(gateId: string);
48
- }
49
- declare class GateAlreadySettled extends GateError {
50
- readonly status: GateStatus;
51
- constructor(gateId: string, status: GateStatus);
52
- }
53
- declare class GateCancelled extends GateError {
54
- readonly reason: string;
55
- constructor(gateId: string, reason: string);
56
- }
57
- declare class GateExpired extends GateError {
58
- constructor(gateId: string);
59
- }
60
- declare class GatePayloadInvalid extends GateError {
61
- readonly issues: string[];
62
- constructor(gateId: string, issues: string[]);
63
- }
64
- declare class GateAborted extends GateError {
65
- constructor(gateId: string, reason: string);
66
- }
2
+ import { G as GateRecord, a as GateStore, B as BaseGateStore, J as JsonSchema } from './base-store-BeRbMZEp.js';
3
+ export { b as GateAborted, c as GateAlreadyExists, d as GateAlreadySettled, e as GateCancelled, f as GateError, g as GateExpired, h as GateInput, i as GateNotFound, j as GatePayloadInvalid, k as GateStatus } from './base-store-BeRbMZEp.js';
67
4
 
68
5
  declare const DEFAULT_POLL_MS = 250;
69
6
  interface OpenGateOptions<T> {
@@ -99,28 +36,6 @@ declare function waitForGate<T = unknown>(store: GateStore, id: string, options?
99
36
  declare function resolveGate(store: GateStore, id: string, payload: unknown): GateRecord;
100
37
  declare function cancelGate(store: GateStore, id: string, reason: string): GateRecord;
101
38
 
102
- /**
103
- * Every settle rule lives here so the memory and SQLite stores cannot drift
104
- * apart. Subclasses only move rows.
105
- */
106
- declare abstract class BaseGateStore implements GateStore {
107
- protected readonly clock: () => number;
108
- protected constructor(clock: () => number);
109
- protected abstract insert(record: GateRecord): void;
110
- protected abstract read(id: string): GateRecord | undefined;
111
- protected abstract update(record: GateRecord): void;
112
- protected abstract readByStatus(status: GateRecord["status"]): GateRecord[];
113
- create(input: GateInput): GateRecord;
114
- get(id: string): GateRecord | undefined;
115
- resolve(id: string, payload: unknown): GateRecord;
116
- cancel(id: string, reason: string): GateRecord;
117
- listPending(): GateRecord[];
118
- private requirePending;
119
- /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
120
- private lapseIfExpired;
121
- protected nowIso(): string;
122
- }
123
-
124
39
  interface MemoryGateStoreOptions {
125
40
  /** Epoch-millis clock, injectable so expiry is testable without waiting. */
126
41
  now?: () => number;
@@ -139,31 +54,6 @@ declare class MemoryGateStore extends BaseGateStore {
139
54
  protected readByStatus(status: GateRecord["status"]): GateRecord[];
140
55
  }
141
56
 
142
- declare const DEFAULT_GATE_TABLE = "hitl_gate";
143
- declare function gateTableDdl(name?: string): string;
144
- /** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */
145
- declare function gateMigration(version: number, name?: string): Migration;
146
- interface SqliteGateStoreOptions {
147
- table?: string;
148
- /** Run `gateMigration` on construction. Off when the product owns its migration list. */
149
- migrate?: boolean;
150
- now?: () => number;
151
- }
152
- /**
153
- * Durable gates. Two instances on the same file are two processes as far as the
154
- * gate is concerned, which is the entire point: whoever resolves the row does
155
- * not have to be whoever opened it.
156
- */
157
- declare class SqliteGateStore extends BaseGateStore {
158
- private readonly db;
159
- private readonly table;
160
- constructor(db: Db, options?: SqliteGateStoreOptions);
161
- protected insert(record: GateRecord): void;
162
- protected read(id: string): GateRecord | undefined;
163
- protected update(record: GateRecord): void;
164
- protected readByStatus(status: GateStatus): GateRecord[];
165
- }
166
-
167
57
  /**
168
58
  * A deliberately small structural check over the JSON Schema subset `z.toJSONSchema`
169
59
  * emits. It exists so the process that resolves a gate can reject a bad payload
@@ -173,4 +63,4 @@ declare class SqliteGateStore extends BaseGateStore {
173
63
  */
174
64
  declare function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path?: string): string[];
175
65
 
176
- export { BaseGateStore, DEFAULT_GATE_TABLE, DEFAULT_POLL_MS, GateAborted, GateAlreadyExists, GateAlreadySettled, GateCancelled, GateError, GateExpired, type GateHandle, type GateInput, GateNotFound, GatePayloadInvalid, type GateRecord, type GateStatus, type GateStore, type JsonSchema, MemoryGateStore, type MemoryGateStoreOptions, type OpenGateOptions, SqliteGateStore, type SqliteGateStoreOptions, type WaitOptions, cancelGate, checkAgainstJsonSchema, gateMigration, gateTableDdl, openGate, resolveGate, waitForGate };
66
+ export { BaseGateStore, DEFAULT_POLL_MS, type GateHandle, GateRecord, GateStore, JsonSchema, MemoryGateStore, type MemoryGateStoreOptions, type OpenGateOptions, type WaitOptions, cancelGate, checkAgainstJsonSchema, openGate, resolveGate, waitForGate };
package/dist/index.js CHANGED
@@ -1,58 +1,18 @@
1
- // src/gate.ts
2
- import { toJSONSchema } from "zod";
3
-
4
- // src/types.ts
5
- var GateError = class extends Error {
6
- constructor(message, gateId) {
7
- super(message);
8
- this.gateId = gateId;
9
- this.name = new.target.name;
10
- }
11
- gateId;
12
- };
13
- var GateNotFound = class extends GateError {
14
- constructor(gateId) {
15
- super(`no gate with id ${gateId}`, gateId);
16
- }
17
- };
18
- var GateAlreadyExists = class extends GateError {
19
- constructor(gateId) {
20
- super(`a gate with id ${gateId} already exists`, gateId);
21
- }
22
- };
23
- var GateAlreadySettled = class extends GateError {
24
- constructor(gateId, status) {
25
- super(`gate ${gateId} is already ${status}`, gateId);
26
- this.status = status;
27
- }
28
- status;
29
- };
30
- var GateCancelled = class extends GateError {
31
- constructor(gateId, reason) {
32
- super(`gate ${gateId} was cancelled: ${reason}`, gateId);
33
- this.reason = reason;
34
- }
35
- reason;
36
- };
37
- var GateExpired = class extends GateError {
38
- constructor(gateId) {
39
- super(`gate ${gateId} expired before anyone resolved it`, gateId);
40
- }
41
- };
42
- var GatePayloadInvalid = class extends GateError {
43
- constructor(gateId, issues) {
44
- super(`payload for gate ${gateId} does not match its schema: ${issues.join("; ")}`, gateId);
45
- this.issues = issues;
46
- }
47
- issues;
48
- };
49
- var GateAborted = class extends GateError {
50
- constructor(gateId, reason) {
51
- super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);
52
- }
53
- };
1
+ import {
2
+ BaseGateStore,
3
+ GateAborted,
4
+ GateAlreadyExists,
5
+ GateAlreadySettled,
6
+ GateCancelled,
7
+ GateError,
8
+ GateExpired,
9
+ GateNotFound,
10
+ GatePayloadInvalid,
11
+ checkAgainstJsonSchema
12
+ } from "./chunk-5P4RFTKR.js";
54
13
 
55
14
  // src/gate.ts
15
+ import { toJSONSchema } from "zod";
56
16
  var DEFAULT_POLL_MS = 250;
57
17
  function openGate(store, options) {
58
18
  const input = { prompt: options.prompt };
@@ -112,150 +72,6 @@ function sleep(ms, signal) {
112
72
  });
113
73
  }
114
74
 
115
- // src/base-store.ts
116
- import { randomUUID } from "crypto";
117
-
118
- // src/json-schema.ts
119
- function checkAgainstJsonSchema(schema, value, path = "$") {
120
- const issues = [];
121
- collect(schema, value, path, issues);
122
- return issues;
123
- }
124
- function collect(schema, value, path, issues) {
125
- if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {
126
- checkUnion(schema.anyOf ?? schema.oneOf, value, path, issues);
127
- return;
128
- }
129
- if ("const" in schema && !Object.is(schema.const, value)) {
130
- issues.push(`${path}: expected ${JSON.stringify(schema.const)}`);
131
- return;
132
- }
133
- if (Array.isArray(schema.enum) && !schema.enum.some((option) => Object.is(option, value))) {
134
- issues.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
135
- return;
136
- }
137
- if (typeof schema.type !== "string") return;
138
- if (!matchesType(schema.type, value)) {
139
- issues.push(`${path}: expected ${schema.type}, received ${describe(value)}`);
140
- return;
141
- }
142
- if (schema.type === "object") checkObject(schema, value, path, issues);
143
- if (schema.type === "array") checkArray(schema, value, path, issues);
144
- }
145
- function checkUnion(branches, value, path, issues) {
146
- const matched = branches.some((branch) => checkAgainstJsonSchema(branch, value, path).length === 0);
147
- if (!matched) issues.push(`${path}: matched none of the ${branches.length} allowed shapes`);
148
- }
149
- function checkObject(schema, value, path, issues) {
150
- const properties = schema.properties ?? {};
151
- for (const key of schema.required ?? []) {
152
- if (!(key in value)) issues.push(`${path}.${key}: required`);
153
- }
154
- if (schema.additionalProperties === false) {
155
- for (const key of Object.keys(value)) {
156
- if (!(key in properties)) issues.push(`${path}.${key}: unexpected property`);
157
- }
158
- }
159
- for (const [key, sub] of Object.entries(properties)) {
160
- if (key in value) collect(sub, value[key], `${path}.${key}`, issues);
161
- }
162
- }
163
- function checkArray(schema, value, path, issues) {
164
- const items = schema.items;
165
- if (!items) return;
166
- value.forEach((entry, index) => collect(items, entry, `${path}[${index}]`, issues));
167
- }
168
- function matchesType(type, value) {
169
- switch (type) {
170
- case "object":
171
- return typeof value === "object" && value !== null && !Array.isArray(value);
172
- case "array":
173
- return Array.isArray(value);
174
- case "integer":
175
- return Number.isInteger(value);
176
- case "number":
177
- return typeof value === "number" && Number.isFinite(value);
178
- case "null":
179
- return value === null;
180
- default:
181
- return typeof value === type;
182
- }
183
- }
184
- function describe(value) {
185
- if (value === null) return "null";
186
- return Array.isArray(value) ? "array" : typeof value;
187
- }
188
-
189
- // src/base-store.ts
190
- var BaseGateStore = class {
191
- constructor(clock) {
192
- this.clock = clock;
193
- }
194
- clock;
195
- create(input) {
196
- const id = input.id ?? randomUUID();
197
- if (this.read(id)) throw new GateAlreadyExists(id);
198
- const record = {
199
- id,
200
- prompt: input.prompt,
201
- schema: input.schema,
202
- status: "pending",
203
- payload: void 0,
204
- reason: void 0,
205
- createdAt: this.nowIso(),
206
- resolvedAt: void 0,
207
- expiresAt: toIso(input.expiresAt)
208
- };
209
- this.insert(record);
210
- return record;
211
- }
212
- get(id) {
213
- const record = this.read(id);
214
- return record ? this.lapseIfExpired(record) : void 0;
215
- }
216
- resolve(id, payload) {
217
- const record = this.requirePending(id);
218
- if (record.schema) {
219
- const issues = checkAgainstJsonSchema(record.schema, payload);
220
- if (issues.length > 0) throw new GatePayloadInvalid(id, issues);
221
- }
222
- const resolved = { ...record, status: "resolved", payload, resolvedAt: this.nowIso() };
223
- this.update(resolved);
224
- return resolved;
225
- }
226
- cancel(id, reason) {
227
- const record = this.requirePending(id);
228
- const cancelled = { ...record, status: "cancelled", reason, resolvedAt: this.nowIso() };
229
- this.update(cancelled);
230
- return cancelled;
231
- }
232
- listPending() {
233
- return this.readByStatus("pending").map((record) => this.lapseIfExpired(record)).filter((record) => record.status === "pending").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
234
- }
235
- requirePending(id) {
236
- const record = this.get(id);
237
- if (!record) throw new GateNotFound(id);
238
- if (record.status === "expired") throw new GateExpired(id);
239
- if (record.status !== "pending") throw new GateAlreadySettled(id, record.status);
240
- return record;
241
- }
242
- /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
243
- lapseIfExpired(record) {
244
- if (record.status !== "pending" || !record.expiresAt) return record;
245
- if (Date.parse(record.expiresAt) > this.clock()) return record;
246
- const expired = { ...record, status: "expired", resolvedAt: this.nowIso() };
247
- this.update(expired);
248
- return expired;
249
- }
250
- nowIso() {
251
- return new Date(this.clock()).toISOString();
252
- }
253
- };
254
- function toIso(value) {
255
- if (value === void 0) return void 0;
256
- return typeof value === "string" ? value : value.toISOString();
257
- }
258
-
259
75
  // src/memory-store.ts
260
76
  var MemoryGateStore = class extends BaseGateStore {
261
77
  rows = /* @__PURE__ */ new Map();
@@ -276,95 +92,8 @@ var MemoryGateStore = class extends BaseGateStore {
276
92
  return [...this.rows.values()].filter((row) => row.status === status).map((row) => ({ ...row }));
277
93
  }
278
94
  };
279
-
280
- // src/sqlite-store.ts
281
- import { quoteIdent, runMigrations } from "@titan-design/store-sqlite";
282
- var DEFAULT_GATE_TABLE = "hitl_gate";
283
- function gateTableDdl(name = DEFAULT_GATE_TABLE) {
284
- const t = quoteIdent(name);
285
- return `
286
- CREATE TABLE IF NOT EXISTS ${t} (
287
- id TEXT PRIMARY KEY,
288
- prompt TEXT NOT NULL,
289
- schema TEXT,
290
- status TEXT NOT NULL DEFAULT 'pending',
291
- payload TEXT,
292
- reason TEXT,
293
- created_at TEXT NOT NULL,
294
- resolved_at TEXT,
295
- expires_at TEXT
296
- );
297
- CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}
298
- ON ${t} (created_at) WHERE status = 'pending';
299
- `;
300
- }
301
- function gateMigration(version, name = DEFAULT_GATE_TABLE) {
302
- return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };
303
- }
304
- var SqliteGateStore = class extends BaseGateStore {
305
- constructor(db, options = {}) {
306
- super(options.now ?? Date.now);
307
- this.db = db;
308
- this.table = options.table ?? DEFAULT_GATE_TABLE;
309
- if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);
310
- }
311
- db;
312
- table;
313
- insert(record) {
314
- this.db.prepare(
315
- `INSERT INTO ${quoteIdent(this.table)}
316
- (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)
317
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
318
- ).run(...columns(record));
319
- }
320
- read(id) {
321
- const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id);
322
- return row ? toRecord(row) : void 0;
323
- }
324
- update(record) {
325
- this.db.prepare(
326
- `UPDATE ${quoteIdent(this.table)}
327
- SET status = ?, payload = ?, reason = ?, resolved_at = ?
328
- WHERE id = ?`
329
- ).run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);
330
- }
331
- readByStatus(status) {
332
- const rows = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`).all(status);
333
- return rows.map(toRecord);
334
- }
335
- };
336
- function columns(record) {
337
- return [
338
- record.id,
339
- record.prompt,
340
- record.schema ? JSON.stringify(record.schema) : null,
341
- record.status,
342
- toJson(record.payload),
343
- record.reason ?? null,
344
- record.createdAt,
345
- record.resolvedAt ?? null,
346
- record.expiresAt ?? null
347
- ];
348
- }
349
- function toJson(payload) {
350
- return payload === void 0 ? null : JSON.stringify(payload);
351
- }
352
- function toRecord(row) {
353
- return {
354
- id: row.id,
355
- prompt: row.prompt,
356
- schema: row.schema ? JSON.parse(row.schema) : void 0,
357
- status: row.status,
358
- payload: row.payload === null ? void 0 : JSON.parse(row.payload),
359
- reason: row.reason ?? void 0,
360
- createdAt: row.created_at,
361
- resolvedAt: row.resolved_at ?? void 0,
362
- expiresAt: row.expires_at ?? void 0
363
- };
364
- }
365
95
  export {
366
96
  BaseGateStore,
367
- DEFAULT_GATE_TABLE,
368
97
  DEFAULT_POLL_MS,
369
98
  GateAborted,
370
99
  GateAlreadyExists,
@@ -375,11 +104,8 @@ export {
375
104
  GateNotFound,
376
105
  GatePayloadInvalid,
377
106
  MemoryGateStore,
378
- SqliteGateStore,
379
107
  cancelGate,
380
108
  checkAgainstJsonSchema,
381
- gateMigration,
382
- gateTableDdl,
383
109
  openGate,
384
110
  resolveGate,
385
111
  waitForGate
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/gate.ts","../src/types.ts","../src/base-store.ts","../src/json-schema.ts","../src/memory-store.ts","../src/sqlite-store.ts"],"sourcesContent":["import { toJSONSchema, type ZodType } from \"zod\";\nimport {\n GateAborted,\n GateCancelled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateRecord,\n type GateStore,\n} from \"./types.js\";\n\nexport const DEFAULT_POLL_MS = 250;\n\nexport interface OpenGateOptions<T> {\n id?: string;\n prompt: string;\n /** Stored as JSON Schema so the resolving process can validate without it, and reused here to type the answer. */\n schema?: ZodType<T>;\n expiresAt?: Date | string;\n}\n\nexport interface WaitOptions<T> {\n pollMs?: number;\n signal?: AbortSignal;\n /** Needed only when re-attaching to a gate this process did not open. */\n schema?: ZodType<T>;\n}\n\nexport interface GateHandle<T> {\n id: string;\n record: GateRecord;\n wait(options?: Omit<WaitOptions<T>, \"schema\">): Promise<T>;\n}\n\n/**\n * Open a gate and hand back something to await. The row exists before this\n * returns, so a crash here leaves a resolvable gate rather than a lost one.\n */\nexport function openGate<T = unknown>(store: GateStore, options: OpenGateOptions<T>): GateHandle<T> {\n const input: Parameters<GateStore[\"create\"]>[0] = { prompt: options.prompt };\n if (options.id !== undefined) input.id = options.id;\n if (options.expiresAt !== undefined) input.expiresAt = options.expiresAt;\n if (options.schema) input.schema = toJSONSchema(options.schema) as Record<string, unknown>;\n\n const record = store.create(input);\n return {\n id: record.id,\n record,\n wait: (waitOptions = {}) => {\n const merged: WaitOptions<T> = { ...waitOptions };\n if (options.schema) merged.schema = options.schema;\n return waitForGate<T>(store, record.id, merged);\n },\n };\n}\n\n/**\n * Await a gate by id from any process, including one that restarted after the\n * gate was opened. Polls rather than subscribes because the resolver may be a\n * different process writing the same SQLite file.\n */\nexport async function waitForGate<T = unknown>(\n store: GateStore,\n id: string,\n options: WaitOptions<T> = {},\n): Promise<T> {\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n for (;;) {\n throwIfAborted(id, options.signal);\n const record = store.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"resolved\") return validate(record, options.schema);\n if (record.status === \"cancelled\") throw new GateCancelled(id, record.reason ?? \"no reason given\");\n if (record.status === \"expired\") throw new GateExpired(id);\n await sleep(pollMs, options.signal);\n }\n}\n\n/** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */\nexport function resolveGate(store: GateStore, id: string, payload: unknown): GateRecord {\n return store.resolve(id, payload);\n}\n\nexport function cancelGate(store: GateStore, id: string, reason: string): GateRecord {\n return store.cancel(id, reason);\n}\n\nfunction validate<T>(record: GateRecord, schema: ZodType<T> | undefined): T {\n if (!schema) return record.payload as T;\n const parsed = schema.safeParse(record.payload);\n if (parsed.success) return parsed.data;\n throw new GatePayloadInvalid(\n record.id,\n parsed.error.issues.map((issue) => `${issue.path.join(\".\") || \"$\"}: ${issue.message}`),\n );\n}\n\nfunction throwIfAborted(id: string, signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new GateAborted(id, String(signal.reason ?? \"aborted\"));\n}\n\nfunction sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(finish, ms);\n signal?.addEventListener(\"abort\", finish, { once: true });\n function finish(): void {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", finish);\n resolve();\n }\n });\n}\n","/** A gate is a row, never a promise: pending work survives the process that opened it. */\nexport type GateStatus = \"pending\" | \"resolved\" | \"cancelled\" | \"expired\";\n\n/** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */\nexport type JsonSchema = Record<string, unknown>;\n\nexport interface GateRecord {\n id: string;\n prompt: string;\n schema: JsonSchema | undefined;\n status: GateStatus;\n payload: unknown;\n /** Why the gate was cancelled; unset for every other status. */\n reason: string | undefined;\n /** ISO-8601 with milliseconds, the shape store-sqlite writes and any surface can send as-is. */\n createdAt: string;\n resolvedAt: string | undefined;\n expiresAt: string | undefined;\n}\n\nexport interface GateInput {\n /** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */\n id?: string;\n prompt: string;\n schema?: JsonSchema;\n expiresAt?: Date | string;\n}\n\n/**\n * Synchronous on purpose: both implementations are local (a Map, and\n * better-sqlite3), and `wait` is the only thing that needs to be async.\n */\nexport interface GateStore {\n create(input: GateInput): GateRecord;\n get(id: string): GateRecord | undefined;\n resolve(id: string, payload: unknown): GateRecord;\n cancel(id: string, reason: string): GateRecord;\n listPending(): GateRecord[];\n}\n\nexport class GateError extends Error {\n constructor(\n message: string,\n readonly gateId: string,\n ) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class GateNotFound extends GateError {\n constructor(gateId: string) {\n super(`no gate with id ${gateId}`, gateId);\n }\n}\n\nexport class GateAlreadyExists extends GateError {\n constructor(gateId: string) {\n super(`a gate with id ${gateId} already exists`, gateId);\n }\n}\n\nexport class GateAlreadySettled extends GateError {\n constructor(\n gateId: string,\n readonly status: GateStatus,\n ) {\n super(`gate ${gateId} is already ${status}`, gateId);\n }\n}\n\nexport class GateCancelled extends GateError {\n constructor(\n gateId: string,\n readonly reason: string,\n ) {\n super(`gate ${gateId} was cancelled: ${reason}`, gateId);\n }\n}\n\nexport class GateExpired extends GateError {\n constructor(gateId: string) {\n super(`gate ${gateId} expired before anyone resolved it`, gateId);\n }\n}\n\nexport class GatePayloadInvalid extends GateError {\n constructor(\n gateId: string,\n readonly issues: string[],\n ) {\n super(`payload for gate ${gateId} does not match its schema: ${issues.join(\"; \")}`, gateId);\n }\n}\n\nexport class GateAborted extends GateError {\n constructor(gateId: string, reason: string) {\n super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport { checkAgainstJsonSchema } from \"./json-schema.js\";\nimport {\n GateAlreadyExists,\n GateAlreadySettled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateInput,\n type GateRecord,\n type GateStore,\n} from \"./types.js\";\n\n/**\n * Every settle rule lives here so the memory and SQLite stores cannot drift\n * apart. Subclasses only move rows.\n */\nexport abstract class BaseGateStore implements GateStore {\n protected constructor(protected readonly clock: () => number) {}\n\n protected abstract insert(record: GateRecord): void;\n protected abstract read(id: string): GateRecord | undefined;\n protected abstract update(record: GateRecord): void;\n protected abstract readByStatus(status: GateRecord[\"status\"]): GateRecord[];\n\n create(input: GateInput): GateRecord {\n const id = input.id ?? randomUUID();\n if (this.read(id)) throw new GateAlreadyExists(id);\n const record: GateRecord = {\n id,\n prompt: input.prompt,\n schema: input.schema,\n status: \"pending\",\n payload: undefined,\n reason: undefined,\n createdAt: this.nowIso(),\n resolvedAt: undefined,\n expiresAt: toIso(input.expiresAt),\n };\n this.insert(record);\n return record;\n }\n\n get(id: string): GateRecord | undefined {\n const record = this.read(id);\n return record ? this.lapseIfExpired(record) : undefined;\n }\n\n resolve(id: string, payload: unknown): GateRecord {\n const record = this.requirePending(id);\n if (record.schema) {\n const issues = checkAgainstJsonSchema(record.schema, payload);\n if (issues.length > 0) throw new GatePayloadInvalid(id, issues);\n }\n const resolved: GateRecord = { ...record, status: \"resolved\", payload, resolvedAt: this.nowIso() };\n this.update(resolved);\n return resolved;\n }\n\n cancel(id: string, reason: string): GateRecord {\n const record = this.requirePending(id);\n const cancelled: GateRecord = { ...record, status: \"cancelled\", reason, resolvedAt: this.nowIso() };\n this.update(cancelled);\n return cancelled;\n }\n\n listPending(): GateRecord[] {\n return this.readByStatus(\"pending\")\n .map((record) => this.lapseIfExpired(record))\n .filter((record) => record.status === \"pending\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }\n\n private requirePending(id: string): GateRecord {\n const record = this.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"expired\") throw new GateExpired(id);\n if (record.status !== \"pending\") throw new GateAlreadySettled(id, record.status);\n return record;\n }\n\n /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */\n private lapseIfExpired(record: GateRecord): GateRecord {\n if (record.status !== \"pending\" || !record.expiresAt) return record;\n if (Date.parse(record.expiresAt) > this.clock()) return record;\n const expired: GateRecord = { ...record, status: \"expired\", resolvedAt: this.nowIso() };\n this.update(expired);\n return expired;\n }\n\n protected nowIso(): string {\n return new Date(this.clock()).toISOString();\n }\n}\n\nfunction toIso(value: Date | string | undefined): string | undefined {\n if (value === undefined) return undefined;\n return typeof value === \"string\" ? value : value.toISOString();\n}\n","import type { JsonSchema } from \"./types.js\";\n\n/**\n * A deliberately small structural check over the JSON Schema subset `z.toJSONSchema`\n * emits. It exists so the process that resolves a gate can reject a bad payload\n * without holding the original zod schema, which by definition lives in another\n * process. The waiter still re-validates with the real zod schema, so this is the\n * boundary check, not the type guarantee.\n */\nexport function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path = \"$\"): string[] {\n const issues: string[] = [];\n collect(schema, value, path, issues);\n return issues;\n}\n\nfunction collect(schema: JsonSchema, value: unknown, path: string, issues: string[]): void {\n if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {\n checkUnion((schema.anyOf ?? schema.oneOf) as JsonSchema[], value, path, issues);\n return;\n }\n if (\"const\" in schema && !Object.is(schema.const, value)) {\n issues.push(`${path}: expected ${JSON.stringify(schema.const)}`);\n return;\n }\n if (Array.isArray(schema.enum) && !schema.enum.some((option) => Object.is(option, value))) {\n issues.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);\n return;\n }\n if (typeof schema.type !== \"string\") return;\n if (!matchesType(schema.type, value)) {\n issues.push(`${path}: expected ${schema.type}, received ${describe(value)}`);\n return;\n }\n if (schema.type === \"object\") checkObject(schema, value as Record<string, unknown>, path, issues);\n if (schema.type === \"array\") checkArray(schema, value as unknown[], path, issues);\n}\n\nfunction checkUnion(branches: JsonSchema[], value: unknown, path: string, issues: string[]): void {\n const matched = branches.some((branch) => checkAgainstJsonSchema(branch, value, path).length === 0);\n if (!matched) issues.push(`${path}: matched none of the ${branches.length} allowed shapes`);\n}\n\nfunction checkObject(schema: JsonSchema, value: Record<string, unknown>, path: string, issues: string[]): void {\n const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;\n for (const key of (schema.required ?? []) as string[]) {\n if (!(key in value)) issues.push(`${path}.${key}: required`);\n }\n if (schema.additionalProperties === false) {\n for (const key of Object.keys(value)) {\n if (!(key in properties)) issues.push(`${path}.${key}: unexpected property`);\n }\n }\n for (const [key, sub] of Object.entries(properties)) {\n if (key in value) collect(sub, value[key], `${path}.${key}`, issues);\n }\n}\n\nfunction checkArray(schema: JsonSchema, value: unknown[], path: string, issues: string[]): void {\n const items = schema.items as JsonSchema | undefined;\n if (!items) return;\n value.forEach((entry, index) => collect(items, entry, `${path}[${index}]`, issues));\n}\n\nfunction matchesType(type: string, value: unknown): boolean {\n switch (type) {\n case \"object\":\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n case \"array\":\n return Array.isArray(value);\n case \"integer\":\n return Number.isInteger(value);\n case \"number\":\n return typeof value === \"number\" && Number.isFinite(value);\n case \"null\":\n return value === null;\n default:\n return typeof value === type;\n }\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n return Array.isArray(value) ? \"array\" : typeof value;\n}\n","import { BaseGateStore } from \"./base-store.js\";\nimport type { GateRecord } from \"./types.js\";\n\nexport interface MemoryGateStoreOptions {\n /** Epoch-millis clock, injectable so expiry is testable without waiting. */\n now?: () => number;\n}\n\n/**\n * In-process gates. Right for tests and for a single-process product that only\n * needs the pause, not the durability; anything that must survive a restart\n * wants `SqliteGateStore`.\n */\nexport class MemoryGateStore extends BaseGateStore {\n private readonly rows = new Map<string, GateRecord>();\n\n constructor(options: MemoryGateStoreOptions = {}) {\n super(options.now ?? Date.now);\n }\n\n protected insert(record: GateRecord): void {\n this.rows.set(record.id, { ...record });\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.rows.get(id);\n return row ? { ...row } : undefined;\n }\n\n protected update(record: GateRecord): void {\n this.rows.set(record.id, { ...record });\n }\n\n protected readByStatus(status: GateRecord[\"status\"]): GateRecord[] {\n return [...this.rows.values()].filter((row) => row.status === status).map((row) => ({ ...row }));\n }\n}\n","import { quoteIdent, runMigrations, type Db, type Migration } from \"@titan-design/store-sqlite\";\nimport { BaseGateStore } from \"./base-store.js\";\nimport type { GateRecord, GateStatus, JsonSchema } from \"./types.js\";\n\nexport const DEFAULT_GATE_TABLE = \"hitl_gate\";\n\nexport function gateTableDdl(name: string = DEFAULT_GATE_TABLE): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n id TEXT PRIMARY KEY,\n prompt TEXT NOT NULL,\n schema TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n payload TEXT,\n reason TEXT,\n created_at TEXT NOT NULL,\n resolved_at TEXT,\n expires_at TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}\n ON ${t} (created_at) WHERE status = 'pending';\n `;\n}\n\n/** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */\nexport function gateMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };\n}\n\nexport interface SqliteGateStoreOptions {\n table?: string;\n /** Run `gateMigration` on construction. Off when the product owns its migration list. */\n migrate?: boolean;\n now?: () => number;\n}\n\ninterface RawGateRow {\n id: string;\n prompt: string;\n schema: string | null;\n status: GateStatus;\n payload: string | null;\n reason: string | null;\n created_at: string;\n resolved_at: string | null;\n expires_at: string | null;\n}\n\n/**\n * Durable gates. Two instances on the same file are two processes as far as the\n * gate is concerned, which is the entire point: whoever resolves the row does\n * not have to be whoever opened it.\n */\nexport class SqliteGateStore extends BaseGateStore {\n private readonly table: string;\n\n constructor(\n private readonly db: Db,\n options: SqliteGateStoreOptions = {},\n ) {\n super(options.now ?? Date.now);\n this.table = options.table ?? DEFAULT_GATE_TABLE;\n if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);\n }\n\n protected insert(record: GateRecord): void {\n this.db\n .prepare(\n `INSERT INTO ${quoteIdent(this.table)}\n (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(...columns(record));\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id) as\n | RawGateRow\n | undefined;\n return row ? toRecord(row) : undefined;\n }\n\n protected update(record: GateRecord): void {\n this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?\n WHERE id = ?`,\n )\n .run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);\n }\n\n protected readByStatus(status: GateStatus): GateRecord[] {\n const rows = this.db\n .prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`)\n .all(status) as RawGateRow[];\n return rows.map(toRecord);\n }\n}\n\nfunction columns(record: GateRecord): unknown[] {\n return [\n record.id,\n record.prompt,\n record.schema ? JSON.stringify(record.schema) : null,\n record.status,\n toJson(record.payload),\n record.reason ?? null,\n record.createdAt,\n record.resolvedAt ?? null,\n record.expiresAt ?? null,\n ];\n}\n\n/** `undefined` and a stored JSON `null` are different states, so only the former becomes a SQL NULL. */\nfunction toJson(payload: unknown): string | null {\n return payload === undefined ? null : JSON.stringify(payload);\n}\n\nfunction toRecord(row: RawGateRow): GateRecord {\n return {\n id: row.id,\n prompt: row.prompt,\n schema: row.schema ? (JSON.parse(row.schema) as JsonSchema) : undefined,\n status: row.status,\n payload: row.payload === null ? undefined : JSON.parse(row.payload),\n reason: row.reason ?? undefined,\n createdAt: row.created_at,\n resolvedAt: row.resolved_at ?? undefined,\n expiresAt: row.expires_at ?? undefined,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/gate.ts","../src/memory-store.ts"],"sourcesContent":["import { toJSONSchema, type ZodType } from \"zod\";\nimport {\n GateAborted,\n GateCancelled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateRecord,\n type GateStore,\n} from \"./types.js\";\n\nexport const DEFAULT_POLL_MS = 250;\n\nexport interface OpenGateOptions<T> {\n id?: string;\n prompt: string;\n /** Stored as JSON Schema so the resolving process can validate without it, and reused here to type the answer. */\n schema?: ZodType<T>;\n expiresAt?: Date | string;\n}\n\nexport interface WaitOptions<T> {\n pollMs?: number;\n signal?: AbortSignal;\n /** Needed only when re-attaching to a gate this process did not open. */\n schema?: ZodType<T>;\n}\n\nexport interface GateHandle<T> {\n id: string;\n record: GateRecord;\n wait(options?: Omit<WaitOptions<T>, \"schema\">): Promise<T>;\n}\n\n/**\n * Open a gate and hand back something to await. The row exists before this\n * returns, so a crash here leaves a resolvable gate rather than a lost one.\n */\nexport function openGate<T = unknown>(store: GateStore, options: OpenGateOptions<T>): GateHandle<T> {\n const input: Parameters<GateStore[\"create\"]>[0] = { prompt: options.prompt };\n if (options.id !== undefined) input.id = options.id;\n if (options.expiresAt !== undefined) input.expiresAt = options.expiresAt;\n if (options.schema) input.schema = toJSONSchema(options.schema) as Record<string, unknown>;\n\n const record = store.create(input);\n return {\n id: record.id,\n record,\n wait: (waitOptions = {}) => {\n const merged: WaitOptions<T> = { ...waitOptions };\n if (options.schema) merged.schema = options.schema;\n return waitForGate<T>(store, record.id, merged);\n },\n };\n}\n\n/**\n * Await a gate by id from any process, including one that restarted after the\n * gate was opened. Polls rather than subscribes because the resolver may be a\n * different process writing the same SQLite file.\n */\nexport async function waitForGate<T = unknown>(\n store: GateStore,\n id: string,\n options: WaitOptions<T> = {},\n): Promise<T> {\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n for (;;) {\n throwIfAborted(id, options.signal);\n const record = store.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"resolved\") return validate(record, options.schema);\n if (record.status === \"cancelled\") throw new GateCancelled(id, record.reason ?? \"no reason given\");\n if (record.status === \"expired\") throw new GateExpired(id);\n await sleep(pollMs, options.signal);\n }\n}\n\n/** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */\nexport function resolveGate(store: GateStore, id: string, payload: unknown): GateRecord {\n return store.resolve(id, payload);\n}\n\nexport function cancelGate(store: GateStore, id: string, reason: string): GateRecord {\n return store.cancel(id, reason);\n}\n\nfunction validate<T>(record: GateRecord, schema: ZodType<T> | undefined): T {\n if (!schema) return record.payload as T;\n const parsed = schema.safeParse(record.payload);\n if (parsed.success) return parsed.data;\n throw new GatePayloadInvalid(\n record.id,\n parsed.error.issues.map((issue) => `${issue.path.join(\".\") || \"$\"}: ${issue.message}`),\n );\n}\n\nfunction throwIfAborted(id: string, signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new GateAborted(id, String(signal.reason ?? \"aborted\"));\n}\n\nfunction sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(finish, ms);\n signal?.addEventListener(\"abort\", finish, { once: true });\n function finish(): void {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", finish);\n resolve();\n }\n });\n}\n","import { BaseGateStore } from \"./base-store.js\";\nimport type { GateRecord } from \"./types.js\";\n\nexport interface MemoryGateStoreOptions {\n /** Epoch-millis clock, injectable so expiry is testable without waiting. */\n now?: () => number;\n}\n\n/**\n * In-process gates. Right for tests and for a single-process product that only\n * needs the pause, not the durability; anything that must survive a restart\n * wants `SqliteGateStore`.\n */\nexport class MemoryGateStore extends BaseGateStore {\n private readonly rows = new Map<string, GateRecord>();\n\n constructor(options: MemoryGateStoreOptions = {}) {\n super(options.now ?? Date.now);\n }\n\n protected insert(record: GateRecord): void {\n this.rows.set(record.id, { ...record });\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.rows.get(id);\n return row ? { ...row } : undefined;\n }\n\n protected update(record: GateRecord): void {\n this.rows.set(record.id, { ...record });\n }\n\n protected readByStatus(status: GateRecord[\"status\"]): GateRecord[] {\n return [...this.rows.values()].filter((row) => row.status === status).map((row) => ({ ...row }));\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,29 @@
1
+ import { Db, Migration } from '@titan-design/store-sqlite';
2
+ import { B as BaseGateStore, G as GateRecord, k as GateStatus } from './base-store-BeRbMZEp.js';
3
+
4
+ declare const DEFAULT_GATE_TABLE = "hitl_gate";
5
+ declare function gateTableDdl(name?: string): string;
6
+ /** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */
7
+ declare function gateMigration(version: number, name?: string): Migration;
8
+ interface SqliteGateStoreOptions {
9
+ table?: string;
10
+ /** Run `gateMigration` on construction. Off when the product owns its migration list. */
11
+ migrate?: boolean;
12
+ now?: () => number;
13
+ }
14
+ /**
15
+ * Durable gates. Two instances on the same file are two processes as far as the
16
+ * gate is concerned, which is the entire point: whoever resolves the row does
17
+ * not have to be whoever opened it.
18
+ */
19
+ declare class SqliteGateStore extends BaseGateStore {
20
+ private readonly db;
21
+ private readonly table;
22
+ constructor(db: Db, options?: SqliteGateStoreOptions);
23
+ protected insert(record: GateRecord): void;
24
+ protected read(id: string): GateRecord | undefined;
25
+ protected update(record: GateRecord): void;
26
+ protected readByStatus(status: GateStatus): GateRecord[];
27
+ }
28
+
29
+ export { DEFAULT_GATE_TABLE, SqliteGateStore, type SqliteGateStoreOptions, gateMigration, gateTableDdl };
package/dist/sqlite.js ADDED
@@ -0,0 +1,96 @@
1
+ import {
2
+ BaseGateStore
3
+ } from "./chunk-5P4RFTKR.js";
4
+
5
+ // src/sqlite-store.ts
6
+ import { quoteIdent, runMigrations } from "@titan-design/store-sqlite";
7
+ var DEFAULT_GATE_TABLE = "hitl_gate";
8
+ function gateTableDdl(name = DEFAULT_GATE_TABLE) {
9
+ const t = quoteIdent(name);
10
+ return `
11
+ CREATE TABLE IF NOT EXISTS ${t} (
12
+ id TEXT PRIMARY KEY,
13
+ prompt TEXT NOT NULL,
14
+ schema TEXT,
15
+ status TEXT NOT NULL DEFAULT 'pending',
16
+ payload TEXT,
17
+ reason TEXT,
18
+ created_at TEXT NOT NULL,
19
+ resolved_at TEXT,
20
+ expires_at TEXT
21
+ );
22
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}
23
+ ON ${t} (created_at) WHERE status = 'pending';
24
+ `;
25
+ }
26
+ function gateMigration(version, name = DEFAULT_GATE_TABLE) {
27
+ return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };
28
+ }
29
+ var SqliteGateStore = class extends BaseGateStore {
30
+ constructor(db, options = {}) {
31
+ super(options.now ?? Date.now);
32
+ this.db = db;
33
+ this.table = options.table ?? DEFAULT_GATE_TABLE;
34
+ if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);
35
+ }
36
+ db;
37
+ table;
38
+ insert(record) {
39
+ this.db.prepare(
40
+ `INSERT INTO ${quoteIdent(this.table)}
41
+ (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)
42
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
43
+ ).run(...columns(record));
44
+ }
45
+ read(id) {
46
+ const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id);
47
+ return row ? toRecord(row) : void 0;
48
+ }
49
+ update(record) {
50
+ this.db.prepare(
51
+ `UPDATE ${quoteIdent(this.table)}
52
+ SET status = ?, payload = ?, reason = ?, resolved_at = ?
53
+ WHERE id = ?`
54
+ ).run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);
55
+ }
56
+ readByStatus(status) {
57
+ const rows = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`).all(status);
58
+ return rows.map(toRecord);
59
+ }
60
+ };
61
+ function columns(record) {
62
+ return [
63
+ record.id,
64
+ record.prompt,
65
+ record.schema ? JSON.stringify(record.schema) : null,
66
+ record.status,
67
+ toJson(record.payload),
68
+ record.reason ?? null,
69
+ record.createdAt,
70
+ record.resolvedAt ?? null,
71
+ record.expiresAt ?? null
72
+ ];
73
+ }
74
+ function toJson(payload) {
75
+ return payload === void 0 ? null : JSON.stringify(payload);
76
+ }
77
+ function toRecord(row) {
78
+ return {
79
+ id: row.id,
80
+ prompt: row.prompt,
81
+ schema: row.schema ? JSON.parse(row.schema) : void 0,
82
+ status: row.status,
83
+ payload: row.payload === null ? void 0 : JSON.parse(row.payload),
84
+ reason: row.reason ?? void 0,
85
+ createdAt: row.created_at,
86
+ resolvedAt: row.resolved_at ?? void 0,
87
+ expiresAt: row.expires_at ?? void 0
88
+ };
89
+ }
90
+ export {
91
+ DEFAULT_GATE_TABLE,
92
+ SqliteGateStore,
93
+ gateMigration,
94
+ gateTableDdl
95
+ };
96
+ //# sourceMappingURL=sqlite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sqlite-store.ts"],"sourcesContent":["import { quoteIdent, runMigrations, type Db, type Migration } from \"@titan-design/store-sqlite\";\nimport { BaseGateStore } from \"./base-store.js\";\nimport type { GateRecord, GateStatus, JsonSchema } from \"./types.js\";\n\nexport const DEFAULT_GATE_TABLE = \"hitl_gate\";\n\nexport function gateTableDdl(name: string = DEFAULT_GATE_TABLE): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n id TEXT PRIMARY KEY,\n prompt TEXT NOT NULL,\n schema TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n payload TEXT,\n reason TEXT,\n created_at TEXT NOT NULL,\n resolved_at TEXT,\n expires_at TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}\n ON ${t} (created_at) WHERE status = 'pending';\n `;\n}\n\n/** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */\nexport function gateMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };\n}\n\nexport interface SqliteGateStoreOptions {\n table?: string;\n /** Run `gateMigration` on construction. Off when the product owns its migration list. */\n migrate?: boolean;\n now?: () => number;\n}\n\ninterface RawGateRow {\n id: string;\n prompt: string;\n schema: string | null;\n status: GateStatus;\n payload: string | null;\n reason: string | null;\n created_at: string;\n resolved_at: string | null;\n expires_at: string | null;\n}\n\n/**\n * Durable gates. Two instances on the same file are two processes as far as the\n * gate is concerned, which is the entire point: whoever resolves the row does\n * not have to be whoever opened it.\n */\nexport class SqliteGateStore extends BaseGateStore {\n private readonly table: string;\n\n constructor(\n private readonly db: Db,\n options: SqliteGateStoreOptions = {},\n ) {\n super(options.now ?? Date.now);\n this.table = options.table ?? DEFAULT_GATE_TABLE;\n if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);\n }\n\n protected insert(record: GateRecord): void {\n this.db\n .prepare(\n `INSERT INTO ${quoteIdent(this.table)}\n (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(...columns(record));\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id) as\n | RawGateRow\n | undefined;\n return row ? toRecord(row) : undefined;\n }\n\n protected update(record: GateRecord): void {\n this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?\n WHERE id = ?`,\n )\n .run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);\n }\n\n protected readByStatus(status: GateStatus): GateRecord[] {\n const rows = this.db\n .prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`)\n .all(status) as RawGateRow[];\n return rows.map(toRecord);\n }\n}\n\nfunction columns(record: GateRecord): unknown[] {\n return [\n record.id,\n record.prompt,\n record.schema ? JSON.stringify(record.schema) : null,\n record.status,\n toJson(record.payload),\n record.reason ?? null,\n record.createdAt,\n record.resolvedAt ?? null,\n record.expiresAt ?? null,\n ];\n}\n\n/** `undefined` and a stored JSON `null` are different states, so only the former becomes a SQL NULL. */\nfunction toJson(payload: unknown): string | null {\n return payload === undefined ? null : JSON.stringify(payload);\n}\n\nfunction toRecord(row: RawGateRow): GateRecord {\n return {\n id: row.id,\n prompt: row.prompt,\n schema: row.schema ? (JSON.parse(row.schema) as JsonSchema) : undefined,\n status: row.status,\n payload: row.payload === null ? undefined : JSON.parse(row.payload),\n reason: row.reason ?? undefined,\n createdAt: row.created_at,\n resolvedAt: row.resolved_at ?? undefined,\n expiresAt: row.expires_at ?? undefined,\n };\n}\n"],"mappings":";;;;;AAAA,SAAS,YAAY,qBAA8C;AAI5D,IAAM,qBAAqB;AAE3B,SAAS,aAAa,OAAe,oBAA4B;AACtE,QAAM,IAAI,WAAW,IAAI;AACzB,SAAO;AAAA,iCACwB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAWD,WAAW,GAAG,IAAI,UAAU,CAAC;AAAA,WACnD,CAAC;AAAA;AAEZ;AAGO,SAAS,cAAc,SAAiB,OAAe,oBAA+B;AAC3F,SAAO,EAAE,SAAS,MAAM,QAAQ,IAAI,IAAI,IAAI,CAAC,OAAO,GAAG,KAAK,aAAa,IAAI,CAAC,EAAE;AAClF;AA0BO,IAAM,kBAAN,cAA8B,cAAc;AAAA,EAGjD,YACmB,IACjB,UAAkC,CAAC,GACnC;AACA,UAAM,QAAQ,OAAO,KAAK,GAAG;AAHZ;AAIjB,SAAK,QAAQ,QAAQ,SAAS;AAC9B,QAAI,QAAQ,WAAW,KAAM,eAAc,IAAI,CAAC,cAAc,GAAG,KAAK,KAAK,CAAC,CAAC;AAAA,EAC/E;AAAA,EANmB;AAAA,EAHF;AAAA,EAWP,OAAO,QAA0B;AACzC,SAAK,GACF;AAAA,MACC,eAAe,WAAW,KAAK,KAAK,CAAC;AAAA;AAAA;AAAA,IAGvC,EACC,IAAI,GAAG,QAAQ,MAAM,CAAC;AAAA,EAC3B;AAAA,EAEU,KAAK,IAAoC;AACjD,UAAM,MAAM,KAAK,GAAG,QAAQ,iBAAiB,WAAW,KAAK,KAAK,CAAC,eAAe,EAAE,IAAI,EAAE;AAG1F,WAAO,MAAM,SAAS,GAAG,IAAI;AAAA,EAC/B;AAAA,EAEU,OAAO,QAA0B;AACzC,SAAK,GACF;AAAA,MACC,UAAU,WAAW,KAAK,KAAK,CAAC;AAAA;AAAA;AAAA,IAGlC,EACC,IAAI,OAAO,QAAQ,OAAO,OAAO,OAAO,GAAG,OAAO,UAAU,MAAM,OAAO,cAAc,MAAM,OAAO,EAAE;AAAA,EAC3G;AAAA,EAEU,aAAa,QAAkC;AACvD,UAAM,OAAO,KAAK,GACf,QAAQ,iBAAiB,WAAW,KAAK,KAAK,CAAC,uCAAuC,EACtF,IAAI,MAAM;AACb,WAAO,KAAK,IAAI,QAAQ;AAAA,EAC1B;AACF;AAEA,SAAS,QAAQ,QAA+B;AAC9C,SAAO;AAAA,IACL,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO,SAAS,KAAK,UAAU,OAAO,MAAM,IAAI;AAAA,IAChD,OAAO;AAAA,IACP,OAAO,OAAO,OAAO;AAAA,IACrB,OAAO,UAAU;AAAA,IACjB,OAAO;AAAA,IACP,OAAO,cAAc;AAAA,IACrB,OAAO,aAAa;AAAA,EACtB;AACF;AAGA,SAAS,OAAO,SAAiC;AAC/C,SAAO,YAAY,SAAY,OAAO,KAAK,UAAU,OAAO;AAC9D;AAEA,SAAS,SAAS,KAA6B;AAC7C,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI,SAAU,KAAK,MAAM,IAAI,MAAM,IAAmB;AAAA,IAC9D,QAAQ,IAAI;AAAA,IACZ,SAAS,IAAI,YAAY,OAAO,SAAY,KAAK,MAAM,IAAI,OAAO;AAAA,IAClE,QAAQ,IAAI,UAAU;AAAA,IACtB,WAAW,IAAI;AAAA,IACf,YAAY,IAAI,eAAe;AAAA,IAC/B,WAAW,IAAI,cAAc;AAAA,EAC/B;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@titan-design/hitl",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Human-in-the-loop gate()/resolve() primitive",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js"
12
+ },
13
+ "./sqlite": {
14
+ "types": "./dist/sqlite.d.ts",
15
+ "import": "./dist/sqlite.js"
12
16
  }
13
17
  },
14
18
  "files": [
@@ -31,7 +35,7 @@
31
35
  "access": "public"
32
36
  },
33
37
  "dependencies": {
34
- "@titan-design/store-sqlite": "0.2.1"
38
+ "@titan-design/store-sqlite": "0.3.0"
35
39
  },
36
40
  "peerDependencies": {
37
41
  "zod": "^4.0.0"