@haikit/postgres 0.12.0 → 0.13.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,43 @@ 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
+
55
59
  Handles are numbered per conversation, so every conversation's digests start at
56
60
  `ui_01`.
57
61
 
62
+ ## Notices
63
+
64
+ A notice is one `INSERT … SELECT`, numbered under the conversation's row lock,
65
+ so two notices in one conversation always commit in the order they were
66
+ numbered and a reader resuming after one can never miss an earlier. It takes no
67
+ lease token: a notice lands whoever holds the turn. The same statement sends a
68
+ `NOTIFY` on `haikit_notices` with the conversation id.
69
+
70
+ Give the store a connection that can `LISTEN`, and the events route hears of a
71
+ notice the moment it lands, rather than at its next two-second read. It needs a
72
+ connection of its own: a pooled one goes back to the pool and stops listening.
73
+ PGlite fits as it is (`pgStore(db, { listen: db })`). With `pg`:
74
+
75
+ ```ts
76
+ const listener = new pg.Client({ connectionString: process.env.DATABASE_URL });
77
+ await listener.connect();
78
+
79
+ const store = pgStore(pool, {
80
+ listen: {
81
+ async listen(channel, onNotify) {
82
+ listener.on("notification", (msg) => msg.channel === channel && onNotify(msg.payload ?? ""));
83
+ await listener.query(`LISTEN ${channel}`);
84
+ },
85
+ },
86
+ });
87
+ ```
88
+
89
+ A notice belongs to its conversation and goes when it does: deleting a
90
+ conversation deletes its notices. `sweepOrphans` never touches them.
91
+
58
92
  ## Cleaning up
59
93
 
60
94
  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,EAsDnC,CAAC;AAEF,8FAA8F;AAC9F,wBAAsB,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1D;AA8DD,wBAAgB,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,GAAE,cAAmB,GAAG,YAAY,CAkPjF;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,7 +29,26 @@ 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,
30
35
  updated_at timestamptz NOT NULL DEFAULT now()
36
+ )`,
37
+ // a table created before notices existed gains the columns
38
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS notice_seq integer NOT NULL DEFAULT 0`,
39
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS noticed_through integer NOT NULL DEFAULT 0`,
40
+ // Append-only, and never an orphan: a notice belongs to a conversation that
41
+ // exists, and goes when it does.
42
+ `CREATE TABLE IF NOT EXISTS haikit_notices (
43
+ conversation_id text NOT NULL REFERENCES haikit_conversations(id) ON DELETE CASCADE,
44
+ seq integer NOT NULL,
45
+ created_at timestamptz NOT NULL DEFAULT now(),
46
+ name text NOT NULL,
47
+ version integer NOT NULL,
48
+ payload jsonb NOT NULL,
49
+ model text,
50
+ handle text,
51
+ PRIMARY KEY (conversation_id, seq)
31
52
  )`,
32
53
  // Write-once. Nothing about a payload changes after insert — everything that
33
54
  // does lives on the fenced conversation row (see Conversation.frozen).
@@ -65,8 +86,11 @@ export async function migrate(db) {
65
86
  // a second time throws. Taking text every time removes the guess.
66
87
  const CONVERSATION_COLUMNS = `
67
88
  id, status, messages::text AS messages, handles::text AS handles,
68
- frozen::text AS frozen, pending::text AS pending, lease_token,
89
+ frozen::text AS frozen, pending::text AS pending, lease_token, noticed_through,
69
90
  (extract(epoch FROM lease_until) * 1000)::float8 AS lease_until_ms`;
91
+ const NOTICE_COLUMNS = `
92
+ conversation_id, seq, name, version, payload::text AS payload, model, handle,
93
+ (extract(epoch FROM created_at) * 1000)::float8 AS created_at_ms`;
70
94
  // The window is read as text too: 'Infinity' is how "never" is stored, and
71
95
  // drivers need not agree on turning it into a number.
72
96
  const PAYLOAD_COLUMNS = `
@@ -83,6 +107,17 @@ const toConversation = (row) => ({
83
107
  pending: row.pending == null ? null : json(row.pending),
84
108
  leaseUntil: row.lease_until_ms == null ? null : Number(row.lease_until_ms),
85
109
  leaseToken: row.lease_token,
110
+ noticedThrough: Number(row.noticed_through ?? 0),
111
+ });
112
+ const toNotice = (row) => ({
113
+ conversationId: row.conversation_id,
114
+ seq: Number(row.seq),
115
+ createdAt: Number(row.created_at_ms),
116
+ name: row.name,
117
+ version: Number(row.version),
118
+ payload: json(row.payload),
119
+ model: row.model,
120
+ ...(row.handle == null ? {} : { handle: row.handle }),
86
121
  });
87
122
  const toPayload = (row) => ({
88
123
  handle: row.handle,
@@ -105,6 +140,56 @@ export function pgStore(db, options = {}) {
105
140
  if (!Number.isFinite(leaseMs) || leaseMs <= 0) {
106
141
  throw new RangeError(`leaseMs must be a positive, finite number of milliseconds (got ${leaseMs})`);
107
142
  }
143
+ // One LISTEN for the whole store, taken when something first watches, and
144
+ // each notification handed to whoever is watching that conversation.
145
+ const waiting = new Map();
146
+ let listening = null;
147
+ const listen = (listener) => (listening ??= listener
148
+ .listen(NOTICE_CHANNEL, (id) => {
149
+ for (const wake of waiting.get(id) ?? [])
150
+ wake();
151
+ })
152
+ .catch((err) => {
153
+ // try again on the next watch, rather than never
154
+ listening = null;
155
+ throw err;
156
+ }));
157
+ async function* watch(conversationId, signal) {
158
+ // One wake-up stands for any number of notices: a step resolves once
159
+ // something has landed since the last, and the caller reads them all.
160
+ let landed = false;
161
+ let wake = null;
162
+ const listener = () => {
163
+ landed = true;
164
+ wake?.();
165
+ };
166
+ const onAbort = () => wake?.();
167
+ const set = waiting.get(conversationId) ?? new Set();
168
+ set.add(listener);
169
+ waiting.set(conversationId, set);
170
+ signal.addEventListener("abort", onAbort);
171
+ try {
172
+ await listen(options.listen);
173
+ // A notice that landed before LISTEN took hold sent its NOTIFY to no
174
+ // one, so the first step is free: the caller reads once now.
175
+ landed = true;
176
+ while (!signal.aborted) {
177
+ if (!landed)
178
+ await new Promise((r) => (wake = r));
179
+ wake = null;
180
+ if (signal.aborted)
181
+ return;
182
+ landed = false;
183
+ yield;
184
+ }
185
+ }
186
+ finally {
187
+ signal.removeEventListener("abort", onAbort);
188
+ set.delete(listener);
189
+ if (!set.size)
190
+ waiting.delete(conversationId);
191
+ }
192
+ }
108
193
  /** A fenced write matched nothing. Say which of the two reasons applies. */
109
194
  async function stale(conversationId) {
110
195
  const { rows } = await db.query(`SELECT 1 FROM haikit_conversations WHERE id = $1`, [conversationId]);
@@ -150,6 +235,7 @@ export function pgStore(db, options = {}) {
150
235
  frozen = $6::jsonb,
151
236
  pending = $7::jsonb,
152
237
  lease_until = to_timestamp($8::float8 / 1000),
238
+ noticed_through = $9::integer,
153
239
  updated_at = now()
154
240
  WHERE id = $1 AND lease_token = $2
155
241
  RETURNING id`, [
@@ -162,6 +248,7 @@ export function pgStore(db, options = {}) {
162
248
  // SQL NULL, not the JSON value null — keep "no pending turn" queryable
163
249
  conversation.pending == null ? null : JSON.stringify(conversation.pending),
164
250
  conversation.leaseUntil,
251
+ conversation.noticedThrough ?? 0,
165
252
  ]);
166
253
  if (!rows.length)
167
254
  await stale(conversation.id);
@@ -220,6 +307,45 @@ export function pgStore(db, options = {}) {
220
307
  return record ? [structuredClone(record)] : [];
221
308
  });
222
309
  },
310
+ async putNotice(record) {
311
+ // Number, insert and notify in one statement. The UPDATE takes the next
312
+ // number under the conversation's row lock, so two notices in one
313
+ // conversation commit in the order they were numbered and a reader never
314
+ // sees a later one before an earlier. No conversation, no row from
315
+ // `next`, nothing inserted. Unfenced: no lease token is checked.
316
+ const { rows } = await db.query(`WITH next AS (
317
+ UPDATE haikit_conversations SET notice_seq = notice_seq + 1
318
+ WHERE id = $1::text
319
+ RETURNING notice_seq
320
+ ), stored AS (
321
+ INSERT INTO haikit_notices (conversation_id, seq, name, version, payload, model, handle)
322
+ SELECT $1::text, notice_seq, $2::text, $3::integer, $4::jsonb, $5::text, $6::text FROM next
323
+ RETURNING ${NOTICE_COLUMNS}
324
+ )
325
+ SELECT stored.*, pg_notify('${NOTICE_CHANNEL}', $1::text) FROM stored`, [
326
+ record.conversationId,
327
+ record.name,
328
+ record.version,
329
+ JSON.stringify(record.payload ?? null),
330
+ record.model,
331
+ record.handle ?? null,
332
+ ]);
333
+ if (!rows.length)
334
+ throw new Error(`conversation ${record.conversationId} does not exist`);
335
+ return toNotice(rows[0]);
336
+ },
337
+ async getNotices(conversationId, after, limit) {
338
+ const { rows } = await db.query(`SELECT ${NOTICE_COLUMNS} FROM haikit_notices
339
+ WHERE conversation_id = $1 AND seq > $2::bigint
340
+ ORDER BY seq
341
+ LIMIT $3::integer`, [conversationId, after, limit ?? null]);
342
+ if (rows.length)
343
+ return rows.map(toNotice);
344
+ // nothing to return: say which of the two reasons applies
345
+ const exists = await db.query(`SELECT 1 FROM haikit_conversations WHERE id = $1`, [conversationId]);
346
+ return exists.rows.length ? [] : null;
347
+ },
348
+ ...(options.listen ? { watch } : {}),
223
349
  };
224
350
  }
225
351
  /**
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.13.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.13.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,27 @@ 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,
61
83
  updated_at timestamptz NOT NULL DEFAULT now()
62
84
  )`,
85
+ // a table created before notices existed gains the columns
86
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS notice_seq integer NOT NULL DEFAULT 0`,
87
+ `ALTER TABLE haikit_conversations ADD COLUMN IF NOT EXISTS noticed_through integer NOT NULL DEFAULT 0`,
88
+ // Append-only, and never an orphan: a notice belongs to a conversation that
89
+ // exists, and goes when it does.
90
+ `CREATE TABLE IF NOT EXISTS haikit_notices (
91
+ conversation_id text NOT NULL REFERENCES haikit_conversations(id) ON DELETE CASCADE,
92
+ seq integer NOT NULL,
93
+ created_at timestamptz NOT NULL DEFAULT now(),
94
+ name text NOT NULL,
95
+ version integer NOT NULL,
96
+ payload jsonb NOT NULL,
97
+ model text,
98
+ handle text,
99
+ PRIMARY KEY (conversation_id, seq)
100
+ )`,
63
101
  // Write-once. Nothing about a payload changes after insert — everything that
64
102
  // does lives on the fenced conversation row (see Conversation.frozen).
65
103
  `CREATE TABLE IF NOT EXISTS haikit_payloads (
@@ -97,9 +135,13 @@ export async function migrate(db: Queryable): Promise<void> {
97
135
  // a second time throws. Taking text every time removes the guess.
98
136
  const CONVERSATION_COLUMNS = `
99
137
  id, status, messages::text AS messages, handles::text AS handles,
100
- frozen::text AS frozen, pending::text AS pending, lease_token,
138
+ frozen::text AS frozen, pending::text AS pending, lease_token, noticed_through,
101
139
  (extract(epoch FROM lease_until) * 1000)::float8 AS lease_until_ms`;
102
140
 
141
+ const NOTICE_COLUMNS = `
142
+ conversation_id, seq, name, version, payload::text AS payload, model, handle,
143
+ (extract(epoch FROM created_at) * 1000)::float8 AS created_at_ms`;
144
+
103
145
  // The window is read as text too: 'Infinity' is how "never" is stored, and
104
146
  // drivers need not agree on turning it into a number.
105
147
  const PAYLOAD_COLUMNS = `
@@ -118,6 +160,18 @@ const toConversation = (row: any): Conversation => ({
118
160
  pending: row.pending == null ? null : json(row.pending),
119
161
  leaseUntil: row.lease_until_ms == null ? null : Number(row.lease_until_ms),
120
162
  leaseToken: row.lease_token,
163
+ noticedThrough: Number(row.noticed_through ?? 0),
164
+ });
165
+
166
+ const toNotice = (row: any): NoticeRecord => ({
167
+ conversationId: row.conversation_id,
168
+ seq: Number(row.seq),
169
+ createdAt: Number(row.created_at_ms),
170
+ name: row.name,
171
+ version: Number(row.version),
172
+ payload: json(row.payload),
173
+ model: row.model,
174
+ ...(row.handle == null ? {} : { handle: row.handle }),
121
175
  });
122
176
 
123
177
  const toPayload = (row: any): PayloadRecord => ({
@@ -145,6 +199,54 @@ export function pgStore(db: Queryable, options: PgStoreOptions = {}): StoreAdapt
145
199
  throw new RangeError(`leaseMs must be a positive, finite number of milliseconds (got ${leaseMs})`);
146
200
  }
147
201
 
202
+ // One LISTEN for the whole store, taken when something first watches, and
203
+ // each notification handed to whoever is watching that conversation.
204
+ const waiting = new Map<string, Set<() => void>>();
205
+ let listening: Promise<unknown> | null = null;
206
+ const listen = (listener: Listener) =>
207
+ (listening ??= listener
208
+ .listen(NOTICE_CHANNEL, (id) => {
209
+ for (const wake of waiting.get(id) ?? []) wake();
210
+ })
211
+ .catch((err) => {
212
+ // try again on the next watch, rather than never
213
+ listening = null;
214
+ throw err;
215
+ }));
216
+
217
+ async function* watch(conversationId: string, signal: AbortSignal): AsyncIterable<void> {
218
+ // One wake-up stands for any number of notices: a step resolves once
219
+ // something has landed since the last, and the caller reads them all.
220
+ let landed = false;
221
+ let wake: (() => void) | null = null;
222
+ const listener = () => {
223
+ landed = true;
224
+ wake?.();
225
+ };
226
+ const onAbort = () => wake?.();
227
+ const set = waiting.get(conversationId) ?? new Set();
228
+ set.add(listener);
229
+ waiting.set(conversationId, set);
230
+ signal.addEventListener("abort", onAbort);
231
+ try {
232
+ await listen(options.listen!);
233
+ // A notice that landed before LISTEN took hold sent its NOTIFY to no
234
+ // one, so the first step is free: the caller reads once now.
235
+ landed = true;
236
+ while (!signal.aborted) {
237
+ if (!landed) await new Promise<void>((r) => (wake = r));
238
+ wake = null;
239
+ if (signal.aborted) return;
240
+ landed = false;
241
+ yield;
242
+ }
243
+ } finally {
244
+ signal.removeEventListener("abort", onAbort);
245
+ set.delete(listener);
246
+ if (!set.size) waiting.delete(conversationId);
247
+ }
248
+ }
249
+
148
250
  /** A fenced write matched nothing. Say which of the two reasons applies. */
149
251
  async function stale(conversationId: string): Promise<never> {
150
252
  const { rows } = await db.query(`SELECT 1 FROM haikit_conversations WHERE id = $1`, [conversationId]);
@@ -199,6 +301,7 @@ export function pgStore(db: Queryable, options: PgStoreOptions = {}): StoreAdapt
199
301
  frozen = $6::jsonb,
200
302
  pending = $7::jsonb,
201
303
  lease_until = to_timestamp($8::float8 / 1000),
304
+ noticed_through = $9::integer,
202
305
  updated_at = now()
203
306
  WHERE id = $1 AND lease_token = $2
204
307
  RETURNING id`,
@@ -212,6 +315,7 @@ export function pgStore(db: Queryable, options: PgStoreOptions = {}): StoreAdapt
212
315
  // SQL NULL, not the JSON value null — keep "no pending turn" queryable
213
316
  conversation.pending == null ? null : JSON.stringify(conversation.pending),
214
317
  conversation.leaseUntil,
318
+ conversation.noticedThrough ?? 0,
215
319
  ],
216
320
  );
217
321
  if (!rows.length) await stale(conversation.id);
@@ -280,6 +384,52 @@ export function pgStore(db: Queryable, options: PgStoreOptions = {}): StoreAdapt
280
384
  return record ? [structuredClone(record)] : [];
281
385
  });
282
386
  },
387
+
388
+ async putNotice(record) {
389
+ // Number, insert and notify in one statement. The UPDATE takes the next
390
+ // number under the conversation's row lock, so two notices in one
391
+ // conversation commit in the order they were numbered and a reader never
392
+ // sees a later one before an earlier. No conversation, no row from
393
+ // `next`, nothing inserted. Unfenced: no lease token is checked.
394
+ const { rows } = await db.query(
395
+ `WITH next AS (
396
+ UPDATE haikit_conversations SET notice_seq = notice_seq + 1
397
+ WHERE id = $1::text
398
+ RETURNING notice_seq
399
+ ), stored AS (
400
+ INSERT INTO haikit_notices (conversation_id, seq, name, version, payload, model, handle)
401
+ SELECT $1::text, notice_seq, $2::text, $3::integer, $4::jsonb, $5::text, $6::text FROM next
402
+ RETURNING ${NOTICE_COLUMNS}
403
+ )
404
+ SELECT stored.*, pg_notify('${NOTICE_CHANNEL}', $1::text) FROM stored`,
405
+ [
406
+ record.conversationId,
407
+ record.name,
408
+ record.version,
409
+ JSON.stringify(record.payload ?? null),
410
+ record.model,
411
+ record.handle ?? null,
412
+ ],
413
+ );
414
+ if (!rows.length) throw new Error(`conversation ${record.conversationId} does not exist`);
415
+ return toNotice(rows[0]);
416
+ },
417
+
418
+ async getNotices(conversationId, after, limit) {
419
+ const { rows } = await db.query(
420
+ `SELECT ${NOTICE_COLUMNS} FROM haikit_notices
421
+ WHERE conversation_id = $1 AND seq > $2::bigint
422
+ ORDER BY seq
423
+ LIMIT $3::integer`,
424
+ [conversationId, after, limit ?? null],
425
+ );
426
+ if (rows.length) return rows.map(toNotice);
427
+ // nothing to return: say which of the two reasons applies
428
+ const exists = await db.query(`SELECT 1 FROM haikit_conversations WHERE id = $1`, [conversationId]);
429
+ return exists.rows.length ? [] : null;
430
+ },
431
+
432
+ ...(options.listen ? { watch } : {}),
283
433
  };
284
434
  }
285
435