@titan-design/hitl 0.1.1 → 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 +16 -3
- package/dist/base-store-BeRbMZEp.d.ts +87 -0
- package/dist/chunk-5P4RFTKR.js +205 -0
- package/dist/chunk-5P4RFTKR.js.map +1 -0
- package/dist/index.d.ts +3 -113
- package/dist/index.js +13 -287
- package/dist/index.js.map +1 -1
- package/dist/sqlite.d.ts +29 -0
- package/dist/sqlite.js +96 -0
- package/dist/sqlite.js.map +1 -0
- package/package.json +6 -2
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).
|
|
8
|
-
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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":";;;;;;;;;;;;;;AAAA,SAAS,oBAAkC;AAWpC,IAAM,kBAAkB;AA2BxB,SAAS,SAAsB,OAAkB,SAA4C;AAClG,QAAM,QAA4C,EAAE,QAAQ,QAAQ,OAAO;AAC3E,MAAI,QAAQ,OAAO,OAAW,OAAM,KAAK,QAAQ;AACjD,MAAI,QAAQ,cAAc,OAAW,OAAM,YAAY,QAAQ;AAC/D,MAAI,QAAQ,OAAQ,OAAM,SAAS,aAAa,QAAQ,MAAM;AAE9D,QAAM,SAAS,MAAM,OAAO,KAAK;AACjC,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX;AAAA,IACA,MAAM,CAAC,cAAc,CAAC,MAAM;AAC1B,YAAM,SAAyB,EAAE,GAAG,YAAY;AAChD,UAAI,QAAQ,OAAQ,QAAO,SAAS,QAAQ;AAC5C,aAAO,YAAe,OAAO,OAAO,IAAI,MAAM;AAAA,IAChD;AAAA,EACF;AACF;AAOA,eAAsB,YACpB,OACA,IACA,UAA0B,CAAC,GACf;AACZ,QAAM,SAAS,QAAQ,UAAU;AACjC,aAAS;AACP,mBAAe,IAAI,QAAQ,MAAM;AACjC,UAAM,SAAS,MAAM,IAAI,EAAE;AAC3B,QAAI,CAAC,OAAQ,OAAM,IAAI,aAAa,EAAE;AACtC,QAAI,OAAO,WAAW,WAAY,QAAO,SAAS,QAAQ,QAAQ,MAAM;AACxE,QAAI,OAAO,WAAW,YAAa,OAAM,IAAI,cAAc,IAAI,OAAO,UAAU,iBAAiB;AACjG,QAAI,OAAO,WAAW,UAAW,OAAM,IAAI,YAAY,EAAE;AACzD,UAAM,MAAM,QAAQ,QAAQ,MAAM;AAAA,EACpC;AACF;AAGO,SAAS,YAAY,OAAkB,IAAY,SAA8B;AACtF,SAAO,MAAM,QAAQ,IAAI,OAAO;AAClC;AAEO,SAAS,WAAW,OAAkB,IAAY,QAA4B;AACnF,SAAO,MAAM,OAAO,IAAI,MAAM;AAChC;AAEA,SAAS,SAAY,QAAoB,QAAmC;AAC1E,MAAI,CAAC,OAAQ,QAAO,OAAO;AAC3B,QAAM,SAAS,OAAO,UAAU,OAAO,OAAO;AAC9C,MAAI,OAAO,QAAS,QAAO,OAAO;AAClC,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,IACP,OAAO,MAAM,OAAO,IAAI,CAAC,UAAU,GAAG,MAAM,KAAK,KAAK,GAAG,KAAK,GAAG,KAAK,MAAM,OAAO,EAAE;AAAA,EACvF;AACF;AAEA,SAAS,eAAe,IAAY,QAAuC;AACzE,MAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,IAAI,OAAO,OAAO,UAAU,SAAS,CAAC;AACnF;AAEA,SAAS,MAAM,IAAY,QAAgD;AACzE,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,QAAQ,WAAW,QAAQ,EAAE;AACnC,YAAQ,iBAAiB,SAAS,QAAQ,EAAE,MAAM,KAAK,CAAC;AACxD,aAAS,SAAe;AACtB,mBAAa,KAAK;AAClB,cAAQ,oBAAoB,SAAS,MAAM;AAC3C,cAAQ;AAAA,IACV;AAAA,EACF,CAAC;AACH;;;AClGO,IAAM,kBAAN,cAA8B,cAAc;AAAA,EAChC,OAAO,oBAAI,IAAwB;AAAA,EAEpD,YAAY,UAAkC,CAAC,GAAG;AAChD,UAAM,QAAQ,OAAO,KAAK,GAAG;AAAA,EAC/B;AAAA,EAEU,OAAO,QAA0B;AACzC,SAAK,KAAK,IAAI,OAAO,IAAI,EAAE,GAAG,OAAO,CAAC;AAAA,EACxC;AAAA,EAEU,KAAK,IAAoC;AACjD,UAAM,MAAM,KAAK,KAAK,IAAI,EAAE;AAC5B,WAAO,MAAM,EAAE,GAAG,IAAI,IAAI;AAAA,EAC5B;AAAA,EAEU,OAAO,QAA0B;AACzC,SAAK,KAAK,IAAI,OAAO,IAAI,EAAE,GAAG,OAAO,CAAC;AAAA,EACxC;AAAA,EAEU,aAAa,QAA4C;AACjE,WAAO,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,EAAE,OAAO,CAAC,QAAQ,IAAI,WAAW,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,EAAE;AAAA,EACjG;AACF;","names":[]}
|
package/dist/sqlite.d.ts
ADDED
|
@@ -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.
|
|
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.
|
|
38
|
+
"@titan-design/store-sqlite": "0.3.0"
|
|
35
39
|
},
|
|
36
40
|
"peerDependencies": {
|
|
37
41
|
"zod": "^4.0.0"
|