@haikit/postgres 0.12.0 → 0.14.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
@@ -40,7 +40,7 @@ exactly one to win.
40
40
 
41
41
  ## Schema
42
42
 
43
- Two tables, `haikit_conversations` and `haikit_payloads`. `migrate()` creates
43
+ Three tables, `haikit_conversations`, `haikit_payloads` and `haikit_notices`. `migrate()` creates
44
44
  them if they do not exist and brings existing ones up to date; if you use your
45
45
  own migration tool, the statements are exported as `schema`.
46
46
 
@@ -52,9 +52,46 @@ With your own migration tool, apply the new `ALTER TABLE` from `schema` before
52
52
  deploying. Rows written before the column existed are held to the window your
53
53
  code declares.
54
54
 
55
+ **Upgrading to 0.12:** `migrate()` adds `haikit_notices`, and `notice_seq` and
56
+ `noticed_through` to `haikit_conversations`, for `hai.notify`. As before, if
57
+ you call `migrate()` at startup there is nothing to do.
58
+
59
+ **Upgrading to 0.13:** `migrate()` adds `kind` to `haikit_notices`, and
60
+ `wakes` and `woke_through` to `haikit_conversations`, for wake notices.
61
+
55
62
  Handles are numbered per conversation, so every conversation's digests start at
56
63
  `ui_01`.
57
64
 
65
+ ## Notices
66
+
67
+ A notice is one `INSERT … SELECT`, numbered under the conversation's row lock,
68
+ so two notices in one conversation always commit in the order they were
69
+ numbered and a reader resuming after one can never miss an earlier. It takes no
70
+ lease token: a notice lands whoever holds the turn. The same statement sends a
71
+ `NOTIFY` on `haikit_notices` with the conversation id.
72
+
73
+ Give the store a connection that can `LISTEN`, and the events route hears of a
74
+ notice the moment it lands, rather than at its next two-second read. It needs a
75
+ connection of its own: a pooled one goes back to the pool and stops listening.
76
+ PGlite fits as it is (`pgStore(db, { listen: db })`). With `pg`:
77
+
78
+ ```ts
79
+ const listener = new pg.Client({ connectionString: process.env.DATABASE_URL });
80
+ await listener.connect();
81
+
82
+ const store = pgStore(pool, {
83
+ listen: {
84
+ async listen(channel, onNotify) {
85
+ listener.on("notification", (msg) => msg.channel === channel && onNotify(msg.payload ?? ""));
86
+ await listener.query(`LISTEN ${channel}`);
87
+ },
88
+ },
89
+ });
90
+ ```
91
+
92
+ A notice belongs to its conversation and goes when it does: deleting a
93
+ conversation deletes its notices. `sweepOrphans` never touches them.
94
+
58
95
  ## Cleaning up
59
96
 
60
97
  A turn that is overtaken mid-flight can leave payload rows that the surviving
package/dist/index.d.ts CHANGED
@@ -32,7 +32,23 @@ export interface PgStoreOptions {
32
32
  * which is safe (its writes are fenced out) but wasteful.
33
33
  */
34
34
  leaseMs?: number;
35
+ /**
36
+ * A connection that can `LISTEN`, so the events route hears of a notice the
37
+ * moment it lands, rather than at its next read a couple of seconds on. It needs a
38
+ * connection of its own: a pooled one goes back to the pool and stops
39
+ * listening. PGlite's `listen` fits as it is; see the README for `pg`.
40
+ */
41
+ listen?: Listener;
42
+ }
43
+ /**
44
+ * Something that can `LISTEN` on a channel and report each `NOTIFY` payload.
45
+ * Resolves once listening, to a function that stops.
46
+ */
47
+ export interface Listener {
48
+ listen(channel: string, onNotify: (payload: string) => void): Promise<unknown>;
35
49
  }
50
+ /** The channel `putNotice` notifies on, with the conversation id as payload. */
51
+ export declare const NOTICE_CHANNEL = "haikit_notices";
36
52
  /**
37
53
  * The schema, one statement per entry. `migrate()` runs these; export them to
38
54
  * your own migration tool instead if you have one.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,SAAS,MAAM,EAmCnC,CAAC;AAEF,8FAA8F;AAC9F,wBAAsB,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1D;AA8CD,wBAAgB,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,GAAE,cAAmB,GAAG,YAAY,CAkJjF;AAED,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAYrG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAChF;AAID,gFAAgF;AAChF,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,SAAS,MAAM,EA8DnC,CAAC;AAEF,8FAA8F;AAC9F,wBAAsB,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1D;AAkED,wBAAgB,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,GAAE,cAAmB,GAAG,YAAY,CAuPjF;AAED,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAYrG"}
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@
12
12
  */
13
13
  import { ConversationBusy, StaleLease, } from "@haikit/core";
14
14
  const LEASE_MS = 120_000;
15
+ /** The channel `putNotice` notifies on, with the conversation id as payload. */
16
+ export const NOTICE_CHANNEL = "haikit_notices";
15
17
  /**
16
18
  * The schema, one statement per entry. `migrate()` runs these; export them to
17
19
  * your own migration tool instead if you have one.
@@ -27,8 +29,35 @@ export const schema = [
27
29
  lease_until timestamptz,
28
30
  lease_token text,
29
31
  handle_seq integer NOT NULL DEFAULT 0,
32
+ -- the last notice number given out, and the last one its history took in
33
+ notice_seq integer NOT NULL DEFAULT 0,
34
+ noticed_through integer NOT NULL DEFAULT 0,
35
+ -- when recent wake turns started, and the last notice that may no longer start one
36
+ wakes jsonb NOT NULL DEFAULT '[]',
37
+ woke_through integer NOT NULL DEFAULT 0,
30
38
  updated_at timestamptz NOT NULL DEFAULT now()
31
39
  )`,
40
+ // a table created before notices existed gains the columns
41
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS notice_seq integer NOT NULL DEFAULT 0`,
42
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS noticed_through integer NOT NULL DEFAULT 0`,
43
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS wakes jsonb NOT NULL DEFAULT '[]'`,
44
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS woke_through integer NOT NULL DEFAULT 0`,
45
+ // Append-only, and never an orphan: a notice belongs to a conversation that
46
+ // exists, and goes when it does.
47
+ `CREATE TABLE IF NOT EXISTS haikit_notices (
48
+ conversation_id text NOT NULL REFERENCES haikit_conversations(id) ON DELETE CASCADE,
49
+ seq integer NOT NULL,
50
+ created_at timestamptz NOT NULL DEFAULT now(),
51
+ name text NOT NULL,
52
+ version integer NOT NULL,
53
+ payload jsonb NOT NULL,
54
+ model text,
55
+ handle text,
56
+ -- 'wake', or NULL for a passive notice
57
+ kind text,
58
+ PRIMARY KEY (conversation_id, seq)
59
+ )`,
60
+ `ALTER TABLE haikit_notices ADD COLUMN IF NOT EXISTS kind text`,
32
61
  // Write-once. Nothing about a payload changes after insert — everything that
33
62
  // does lives on the fenced conversation row (see Conversation.frozen).
34
63
  `CREATE TABLE IF NOT EXISTS haikit_payloads (
@@ -65,8 +94,12 @@ export async function migrate(db) {
65
94
  // a second time throws. Taking text every time removes the guess.
66
95
  const CONVERSATION_COLUMNS = `
67
96
  id, status, messages::text AS messages, handles::text AS handles,
68
- frozen::text AS frozen, pending::text AS pending, lease_token,
97
+ frozen::text AS frozen, pending::text AS pending, lease_token, noticed_through,
98
+ wakes::text AS wakes, woke_through,
69
99
  (extract(epoch FROM lease_until) * 1000)::float8 AS lease_until_ms`;
100
+ const NOTICE_COLUMNS = `
101
+ conversation_id, seq, name, version, payload::text AS payload, model, handle, kind,
102
+ (extract(epoch FROM created_at) * 1000)::float8 AS created_at_ms`;
70
103
  // The window is read as text too: 'Infinity' is how "never" is stored, and
71
104
  // drivers need not agree on turning it into a number.
72
105
  const PAYLOAD_COLUMNS = `
@@ -83,6 +116,20 @@ const toConversation = (row) => ({
83
116
  pending: row.pending == null ? null : json(row.pending),
84
117
  leaseUntil: row.lease_until_ms == null ? null : Number(row.lease_until_ms),
85
118
  leaseToken: row.lease_token,
119
+ noticedThrough: Number(row.noticed_through ?? 0),
120
+ wakes: row.wakes == null ? [] : json(row.wakes),
121
+ wokeThrough: Number(row.woke_through ?? 0),
122
+ });
123
+ const toNotice = (row) => ({
124
+ conversationId: row.conversation_id,
125
+ seq: Number(row.seq),
126
+ createdAt: Number(row.created_at_ms),
127
+ name: row.name,
128
+ version: Number(row.version),
129
+ payload: json(row.payload),
130
+ model: row.model,
131
+ ...(row.handle == null ? {} : { handle: row.handle }),
132
+ ...(row.kind === "wake" ? { kind: "wake" } : {}),
86
133
  });
87
134
  const toPayload = (row) => ({
88
135
  handle: row.handle,
@@ -105,6 +152,56 @@ export function pgStore(db, options = {}) {
105
152
  if (!Number.isFinite(leaseMs) || leaseMs <= 0) {
106
153
  throw new RangeError(`leaseMs must be a positive, finite number of milliseconds (got ${leaseMs})`);
107
154
  }
155
+ // One LISTEN for the whole store, taken when something first watches, and
156
+ // each notification handed to whoever is watching that conversation.
157
+ const waiting = new Map();
158
+ let listening = null;
159
+ const listen = (listener) => (listening ??= listener
160
+ .listen(NOTICE_CHANNEL, (id) => {
161
+ for (const wake of waiting.get(id) ?? [])
162
+ wake();
163
+ })
164
+ .catch((err) => {
165
+ // try again on the next watch, rather than never
166
+ listening = null;
167
+ throw err;
168
+ }));
169
+ async function* watch(conversationId, signal) {
170
+ // One wake-up stands for any number of notices: a step resolves once
171
+ // something has landed since the last, and the caller reads them all.
172
+ let landed = false;
173
+ let wake = null;
174
+ const listener = () => {
175
+ landed = true;
176
+ wake?.();
177
+ };
178
+ const onAbort = () => wake?.();
179
+ const set = waiting.get(conversationId) ?? new Set();
180
+ set.add(listener);
181
+ waiting.set(conversationId, set);
182
+ signal.addEventListener("abort", onAbort);
183
+ try {
184
+ await listen(options.listen);
185
+ // A notice that landed before LISTEN took hold sent its NOTIFY to no
186
+ // one, so the first step is free: the caller reads once now.
187
+ landed = true;
188
+ while (!signal.aborted) {
189
+ if (!landed)
190
+ await new Promise((r) => (wake = r));
191
+ wake = null;
192
+ if (signal.aborted)
193
+ return;
194
+ landed = false;
195
+ yield;
196
+ }
197
+ }
198
+ finally {
199
+ signal.removeEventListener("abort", onAbort);
200
+ set.delete(listener);
201
+ if (!set.size)
202
+ waiting.delete(conversationId);
203
+ }
204
+ }
108
205
  /** A fenced write matched nothing. Say which of the two reasons applies. */
109
206
  async function stale(conversationId) {
110
207
  const { rows } = await db.query(`SELECT 1 FROM haikit_conversations WHERE id = $1`, [conversationId]);
@@ -150,6 +247,9 @@ export function pgStore(db, options = {}) {
150
247
  frozen = $6::jsonb,
151
248
  pending = $7::jsonb,
152
249
  lease_until = to_timestamp($8::float8 / 1000),
250
+ noticed_through = $9::integer,
251
+ wakes = $10::jsonb,
252
+ woke_through = $11::integer,
153
253
  updated_at = now()
154
254
  WHERE id = $1 AND lease_token = $2
155
255
  RETURNING id`, [
@@ -162,6 +262,9 @@ export function pgStore(db, options = {}) {
162
262
  // SQL NULL, not the JSON value null — keep "no pending turn" queryable
163
263
  conversation.pending == null ? null : JSON.stringify(conversation.pending),
164
264
  conversation.leaseUntil,
265
+ conversation.noticedThrough ?? 0,
266
+ JSON.stringify(conversation.wakes ?? []),
267
+ conversation.wokeThrough ?? 0,
165
268
  ]);
166
269
  if (!rows.length)
167
270
  await stale(conversation.id);
@@ -220,6 +323,46 @@ export function pgStore(db, options = {}) {
220
323
  return record ? [structuredClone(record)] : [];
221
324
  });
222
325
  },
326
+ async putNotice(record) {
327
+ // Number, insert and notify in one statement. The UPDATE takes the next
328
+ // number under the conversation's row lock, so two notices in one
329
+ // conversation commit in the order they were numbered and a reader never
330
+ // sees a later one before an earlier. No conversation, no row from
331
+ // `next`, nothing inserted. Unfenced: no lease token is checked.
332
+ const { rows } = await db.query(`WITH next AS (
333
+ UPDATE haikit_conversations SET notice_seq = notice_seq + 1
334
+ WHERE id = $1::text
335
+ RETURNING notice_seq
336
+ ), stored AS (
337
+ INSERT INTO haikit_notices (conversation_id, seq, name, version, payload, model, handle, kind)
338
+ SELECT $1::text, notice_seq, $2::text, $3::integer, $4::jsonb, $5::text, $6::text, $7::text FROM next
339
+ RETURNING ${NOTICE_COLUMNS}
340
+ )
341
+ SELECT stored.*, pg_notify('${NOTICE_CHANNEL}', $1::text) FROM stored`, [
342
+ record.conversationId,
343
+ record.name,
344
+ record.version,
345
+ JSON.stringify(record.payload ?? null),
346
+ record.model,
347
+ record.handle ?? null,
348
+ record.kind === "wake" ? "wake" : null,
349
+ ]);
350
+ if (!rows.length)
351
+ throw new Error(`conversation ${record.conversationId} does not exist`);
352
+ return toNotice(rows[0]);
353
+ },
354
+ async getNotices(conversationId, after, limit) {
355
+ const { rows } = await db.query(`SELECT ${NOTICE_COLUMNS} FROM haikit_notices
356
+ WHERE conversation_id = $1 AND seq > $2::bigint
357
+ ORDER BY seq
358
+ LIMIT $3::integer`, [conversationId, after, limit ?? null]);
359
+ if (rows.length)
360
+ return rows.map(toNotice);
361
+ // nothing to return: say which of the two reasons applies
362
+ const exists = await db.query(`SELECT 1 FROM haikit_conversations WHERE id = $1`, [conversationId]);
363
+ return exists.rows.length ? [] : null;
364
+ },
365
+ ...(options.listen ? { watch } : {}),
223
366
  };
224
367
  }
225
368
  /**
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haikit/postgres",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Postgres store adapter for haikit: durable conversations, fenced turn leases, write-once payloads. Bring your own driver.",
5
5
  "keywords": [
6
6
  "haikit",
@@ -34,7 +34,7 @@
34
34
  "prepack": "npm --prefix ../.. run build"
35
35
  },
36
36
  "dependencies": {
37
- "@haikit/core": "0.12.0"
37
+ "@haikit/core": "0.14.0"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  ConversationBusy,
16
16
  StaleLease,
17
17
  type Conversation,
18
+ type NoticeRecord,
18
19
  type PayloadRecord,
19
20
  type StoreAdapter,
20
21
  } from "@haikit/core";
@@ -39,10 +40,28 @@ export interface PgStoreOptions {
39
40
  * which is safe (its writes are fenced out) but wasteful.
40
41
  */
41
42
  leaseMs?: number;
43
+ /**
44
+ * A connection that can `LISTEN`, so the events route hears of a notice the
45
+ * moment it lands, rather than at its next read a couple of seconds on. It needs a
46
+ * connection of its own: a pooled one goes back to the pool and stops
47
+ * listening. PGlite's `listen` fits as it is; see the README for `pg`.
48
+ */
49
+ listen?: Listener;
50
+ }
51
+
52
+ /**
53
+ * Something that can `LISTEN` on a channel and report each `NOTIFY` payload.
54
+ * Resolves once listening, to a function that stops.
55
+ */
56
+ export interface Listener {
57
+ listen(channel: string, onNotify: (payload: string) => void): Promise<unknown>;
42
58
  }
43
59
 
44
60
  const LEASE_MS = 120_000;
45
61
 
62
+ /** The channel `putNotice` notifies on, with the conversation id as payload. */
63
+ export const NOTICE_CHANNEL = "haikit_notices";
64
+
46
65
  /**
47
66
  * The schema, one statement per entry. `migrate()` runs these; export them to
48
67
  * your own migration tool instead if you have one.
@@ -58,8 +77,35 @@ export const schema: readonly string[] = [
58
77
  lease_until timestamptz,
59
78
  lease_token text,
60
79
  handle_seq integer NOT NULL DEFAULT 0,
80
+ -- the last notice number given out, and the last one its history took in
81
+ notice_seq integer NOT NULL DEFAULT 0,
82
+ noticed_through integer NOT NULL DEFAULT 0,
83
+ -- when recent wake turns started, and the last notice that may no longer start one
84
+ wakes jsonb NOT NULL DEFAULT '[]',
85
+ woke_through integer NOT NULL DEFAULT 0,
61
86
  updated_at timestamptz NOT NULL DEFAULT now()
62
87
  )`,
88
+ // a table created before notices existed gains the columns
89
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS notice_seq integer NOT NULL DEFAULT 0`,
90
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS noticed_through integer NOT NULL DEFAULT 0`,
91
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS wakes jsonb NOT NULL DEFAULT '[]'`,
92
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS woke_through integer NOT NULL DEFAULT 0`,
93
+ // Append-only, and never an orphan: a notice belongs to a conversation that
94
+ // exists, and goes when it does.
95
+ `CREATE TABLE IF NOT EXISTS haikit_notices (
96
+ conversation_id text NOT NULL REFERENCES haikit_conversations(id) ON DELETE CASCADE,
97
+ seq integer NOT NULL,
98
+ created_at timestamptz NOT NULL DEFAULT now(),
99
+ name text NOT NULL,
100
+ version integer NOT NULL,
101
+ payload jsonb NOT NULL,
102
+ model text,
103
+ handle text,
104
+ -- 'wake', or NULL for a passive notice
105
+ kind text,
106
+ PRIMARY KEY (conversation_id, seq)
107
+ )`,
108
+ `ALTER TABLE haikit_notices ADD COLUMN IF NOT EXISTS kind text`,
63
109
  // Write-once. Nothing about a payload changes after insert — everything that
64
110
  // does lives on the fenced conversation row (see Conversation.frozen).
65
111
  `CREATE TABLE IF NOT EXISTS haikit_payloads (
@@ -97,9 +143,14 @@ export async function migrate(db: Queryable): Promise<void> {
97
143
  // a second time throws. Taking text every time removes the guess.
98
144
  const CONVERSATION_COLUMNS = `
99
145
  id, status, messages::text AS messages, handles::text AS handles,
100
- frozen::text AS frozen, pending::text AS pending, lease_token,
146
+ frozen::text AS frozen, pending::text AS pending, lease_token, noticed_through,
147
+ wakes::text AS wakes, woke_through,
101
148
  (extract(epoch FROM lease_until) * 1000)::float8 AS lease_until_ms`;
102
149
 
150
+ const NOTICE_COLUMNS = `
151
+ conversation_id, seq, name, version, payload::text AS payload, model, handle, kind,
152
+ (extract(epoch FROM created_at) * 1000)::float8 AS created_at_ms`;
153
+
103
154
  // The window is read as text too: 'Infinity' is how "never" is stored, and
104
155
  // drivers need not agree on turning it into a number.
105
156
  const PAYLOAD_COLUMNS = `
@@ -118,6 +169,21 @@ const toConversation = (row: any): Conversation => ({
118
169
  pending: row.pending == null ? null : json(row.pending),
119
170
  leaseUntil: row.lease_until_ms == null ? null : Number(row.lease_until_ms),
120
171
  leaseToken: row.lease_token,
172
+ noticedThrough: Number(row.noticed_through ?? 0),
173
+ wakes: row.wakes == null ? [] : json(row.wakes),
174
+ wokeThrough: Number(row.woke_through ?? 0),
175
+ });
176
+
177
+ const toNotice = (row: any): NoticeRecord => ({
178
+ conversationId: row.conversation_id,
179
+ seq: Number(row.seq),
180
+ createdAt: Number(row.created_at_ms),
181
+ name: row.name,
182
+ version: Number(row.version),
183
+ payload: json(row.payload),
184
+ model: row.model,
185
+ ...(row.handle == null ? {} : { handle: row.handle }),
186
+ ...(row.kind === "wake" ? { kind: "wake" as const } : {}),
121
187
  });
122
188
 
123
189
  const toPayload = (row: any): PayloadRecord => ({
@@ -145,6 +211,54 @@ export function pgStore(db: Queryable, options: PgStoreOptions = {}): StoreAdapt
145
211
  throw new RangeError(`leaseMs must be a positive, finite number of milliseconds (got ${leaseMs})`);
146
212
  }
147
213
 
214
+ // One LISTEN for the whole store, taken when something first watches, and
215
+ // each notification handed to whoever is watching that conversation.
216
+ const waiting = new Map<string, Set<() => void>>();
217
+ let listening: Promise<unknown> | null = null;
218
+ const listen = (listener: Listener) =>
219
+ (listening ??= listener
220
+ .listen(NOTICE_CHANNEL, (id) => {
221
+ for (const wake of waiting.get(id) ?? []) wake();
222
+ })
223
+ .catch((err) => {
224
+ // try again on the next watch, rather than never
225
+ listening = null;
226
+ throw err;
227
+ }));
228
+
229
+ async function* watch(conversationId: string, signal: AbortSignal): AsyncIterable<void> {
230
+ // One wake-up stands for any number of notices: a step resolves once
231
+ // something has landed since the last, and the caller reads them all.
232
+ let landed = false;
233
+ let wake: (() => void) | null = null;
234
+ const listener = () => {
235
+ landed = true;
236
+ wake?.();
237
+ };
238
+ const onAbort = () => wake?.();
239
+ const set = waiting.get(conversationId) ?? new Set();
240
+ set.add(listener);
241
+ waiting.set(conversationId, set);
242
+ signal.addEventListener("abort", onAbort);
243
+ try {
244
+ await listen(options.listen!);
245
+ // A notice that landed before LISTEN took hold sent its NOTIFY to no
246
+ // one, so the first step is free: the caller reads once now.
247
+ landed = true;
248
+ while (!signal.aborted) {
249
+ if (!landed) await new Promise<void>((r) => (wake = r));
250
+ wake = null;
251
+ if (signal.aborted) return;
252
+ landed = false;
253
+ yield;
254
+ }
255
+ } finally {
256
+ signal.removeEventListener("abort", onAbort);
257
+ set.delete(listener);
258
+ if (!set.size) waiting.delete(conversationId);
259
+ }
260
+ }
261
+
148
262
  /** A fenced write matched nothing. Say which of the two reasons applies. */
149
263
  async function stale(conversationId: string): Promise<never> {
150
264
  const { rows } = await db.query(`SELECT 1 FROM haikit_conversations WHERE id = $1`, [conversationId]);
@@ -199,6 +313,9 @@ export function pgStore(db: Queryable, options: PgStoreOptions = {}): StoreAdapt
199
313
  frozen = $6::jsonb,
200
314
  pending = $7::jsonb,
201
315
  lease_until = to_timestamp($8::float8 / 1000),
316
+ noticed_through = $9::integer,
317
+ wakes = $10::jsonb,
318
+ woke_through = $11::integer,
202
319
  updated_at = now()
203
320
  WHERE id = $1 AND lease_token = $2
204
321
  RETURNING id`,
@@ -212,6 +329,9 @@ export function pgStore(db: Queryable, options: PgStoreOptions = {}): StoreAdapt
212
329
  // SQL NULL, not the JSON value null — keep "no pending turn" queryable
213
330
  conversation.pending == null ? null : JSON.stringify(conversation.pending),
214
331
  conversation.leaseUntil,
332
+ conversation.noticedThrough ?? 0,
333
+ JSON.stringify(conversation.wakes ?? []),
334
+ conversation.wokeThrough ?? 0,
215
335
  ],
216
336
  );
217
337
  if (!rows.length) await stale(conversation.id);
@@ -280,6 +400,53 @@ export function pgStore(db: Queryable, options: PgStoreOptions = {}): StoreAdapt
280
400
  return record ? [structuredClone(record)] : [];
281
401
  });
282
402
  },
403
+
404
+ async putNotice(record) {
405
+ // Number, insert and notify in one statement. The UPDATE takes the next
406
+ // number under the conversation's row lock, so two notices in one
407
+ // conversation commit in the order they were numbered and a reader never
408
+ // sees a later one before an earlier. No conversation, no row from
409
+ // `next`, nothing inserted. Unfenced: no lease token is checked.
410
+ const { rows } = await db.query(
411
+ `WITH next AS (
412
+ UPDATE haikit_conversations SET notice_seq = notice_seq + 1
413
+ WHERE id = $1::text
414
+ RETURNING notice_seq
415
+ ), stored AS (
416
+ INSERT INTO haikit_notices (conversation_id, seq, name, version, payload, model, handle, kind)
417
+ SELECT $1::text, notice_seq, $2::text, $3::integer, $4::jsonb, $5::text, $6::text, $7::text FROM next
418
+ RETURNING ${NOTICE_COLUMNS}
419
+ )
420
+ SELECT stored.*, pg_notify('${NOTICE_CHANNEL}', $1::text) FROM stored`,
421
+ [
422
+ record.conversationId,
423
+ record.name,
424
+ record.version,
425
+ JSON.stringify(record.payload ?? null),
426
+ record.model,
427
+ record.handle ?? null,
428
+ record.kind === "wake" ? "wake" : null,
429
+ ],
430
+ );
431
+ if (!rows.length) throw new Error(`conversation ${record.conversationId} does not exist`);
432
+ return toNotice(rows[0]);
433
+ },
434
+
435
+ async getNotices(conversationId, after, limit) {
436
+ const { rows } = await db.query(
437
+ `SELECT ${NOTICE_COLUMNS} FROM haikit_notices
438
+ WHERE conversation_id = $1 AND seq > $2::bigint
439
+ ORDER BY seq
440
+ LIMIT $3::integer`,
441
+ [conversationId, after, limit ?? null],
442
+ );
443
+ if (rows.length) return rows.map(toNotice);
444
+ // nothing to return: say which of the two reasons applies
445
+ const exists = await db.query(`SELECT 1 FROM haikit_conversations WHERE id = $1`, [conversationId]);
446
+ return exists.rows.length ? [] : null;
447
+ },
448
+
449
+ ...(options.listen ? { watch } : {}),
283
450
  };
284
451
  }
285
452