@north-light/sdk 0.3.544 → 0.3.545
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 +4 -0
- package/dist/skill/northlight/SKILL.md +3 -3
- package/dist/stores/postgres.d.ts +86 -0
- package/dist/stores/postgres.js +182 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -176,6 +176,10 @@ CREATE INDEX connection_updated_at_idx ON connection (updated_at, user_id);
|
|
|
176
176
|
|
|
177
177
|
`save` upserts all fields and sets `updated_at` to the database clock. `listStale(before)` selects connections with `updated_at < before`, oldest first; it is a **last save** timestamp, not necessarily last successful refresh if an app also calls `save` for another reason. For idle-token maintenance, schedule `oauth.refreshIdleConnections({olderThanMs: 30 * 24 * 60 * 60 * 1000})` (for example, daily). It selects stale connections, refreshes under the store lock, reloads there to avoid rotating an already-used token, saves a rotated connection, and reports `{refreshed, refused, failures}`. A connection the directory refuses goes to the client's `onRefusal(refusal)` and is then removed (`remove` deletes the row only while it still holds the refused token); `refusal.kind` is `'removed'` (`grant_removed`: the runtime deleted the app's runs; delete what you store for the person), `'paused'` (`grant_suspended`: nothing was deleted; keep the data until they sign in again), or `'reconnect'` (`refresh_token_expired`, `refresh_token_reused`, `grant_revoked`: nothing was deleted; ask them to sign in again). `refusalOf(error)` returns the same `Refusal` from a refused call's error. Load the signing key `crouter-sdk keygen` wrote with `privateKey: await privateKeyFromFile('local/app-key.json')` (Node), or check one from an environment variable with `parsePrivateJwk(text)`; both refuse anything but a private JWK with a `kid`. The SDK does not start a timer. `listStale` returns connections, not timestamps, so a worker needing a strict cutoff recheck must maintain that policy separately. The table contains credentials: restrict database access, encrypt backups, and delete a row when its user disconnects. PostgreSQL `timestamptz` must be returned as a JavaScript `Date` (the `pg` default parser).
|
|
178
178
|
|
|
179
|
+
## PostgreSQL object kit (Node)
|
|
180
|
+
|
|
181
|
+
`PostgresObjectStore` from `@north-light/sdk/stores/postgres` keeps per-person custom objects, the nodes watching them, and a durable publish outbox. `objectKitSchemaSql(tables?)` returns the DDL for the three tables (`watch_object`, `watch_watcher`, `watch_publish` by default) for your migration. `new PostgresObjectStore(pool, {providerId, runtime: (sub) => oauth.appClient(sub), tables?, log?})` has `instance(sub, kind, target, {name, description})`, `watch(objectId, node, machine?)`, `enqueue(client, objectId, {event, summary, state?})` (pass your open transaction's client), `tick(batch?)` and `startPublisher({tick: 5000, batch: 100})`. Delivery is at least once under an advisory lock, backs off to 15 minutes, and drops a row after 24 hours or a permanent refusal (HTTP 400, 404 or 422). Full guide: [Watchable objects](https://sdk.docs.northlight.ai/docs/guides/watchable-objects).
|
|
182
|
+
|
|
179
183
|
## Example application
|
|
180
184
|
|
|
181
185
|
See the complete [localhost SDK demo](https://github.com/Northlight-Systems/crouter/tree/main/examples/localhost-demo) for streaming, structured output, and an HTTP plugin in one local page.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: northlight
|
|
3
3
|
description: "Use when building, debugging or shipping an app that signs people in with Northlight, starts runs on their Northlight runtime, or calls Northlight providers (sms, search, integrations, context, device) with @north-light/sdk."
|
|
4
4
|
metadata:
|
|
5
|
-
sdk-version: "0.3.
|
|
5
|
+
sdk-version: "0.3.545"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Building a Northlight app
|
|
@@ -15,7 +15,7 @@ The default Northlight app is one people text. It gets its own iMessage number t
|
|
|
15
15
|
|
|
16
16
|
[comms.northlight.ai/llms.txt](https://comms.northlight.ai/llms.txt) is the whole Comms guide: read it and follow it rather than this summary. [Reach people by text](https://sdk.docs.northlight.ai/docs/reach-people-by-text.md) shows the webhook handler.
|
|
17
17
|
|
|
18
|
-
1. **Build in fake mode.** `npx @north-light/sdk@latest init <dir>`, then `npm i @north-light/comms
|
|
18
|
+
1. **Build in fake mode.** `npx @north-light/sdk@latest init <dir>`, then `npm i @north-light/comms`. Run `npx @north-light/comms fake --webhook http://localhost:3000/hook`, and play a person with `npx @north-light/comms fake send --from +15550001 --text "hi"`. No account is needed yet.
|
|
19
19
|
2. **Sign up, log in, register.** Steps 1 to 4 of the journey below. Comms signs in with the same app key.
|
|
20
20
|
3. **Connect the app to Comms.** Ask the person to sign in at `https://comms.northlight.ai` and choose **Connect an app**; an admin of the app approves it there. This is done in a browser only.
|
|
21
21
|
4. **Turn Texting on**, on the app's page there or with `crtr comms turn-on`: display name, square avatar, HELP text, test webhook URL. Copy the test signing secret it shows into the server's environment.
|
|
@@ -27,7 +27,7 @@ Never text a person first: Comms refuses a send to anyone who has not texted you
|
|
|
27
27
|
|
|
28
28
|
## First: check this skill matches the SDK
|
|
29
29
|
|
|
30
|
-
This skill is for SDK version `0.3.
|
|
30
|
+
This skill is for SDK version `0.3.545`. If the project has `node_modules/@north-light/sdk/package.json`, compare its `version` with that number. If they differ, run `npx @north-light/sdk skill` in the project, then read the rewritten `.agents/skills/northlight/SKILL.md` (or `.claude/skills/northlight/SKILL.md`) instead of this copy. It needs no account. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
|
|
31
31
|
|
|
32
32
|
## Where things run
|
|
33
33
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AppClient } from '../client.js';
|
|
1
2
|
import type { Connection, ConnectionStore } from '../oauth/index.js';
|
|
2
3
|
interface QueryResult {
|
|
3
4
|
rows: Record<string, unknown>[];
|
|
@@ -29,4 +30,89 @@ export declare class PostgresConnectionStore implements ConnectionStore {
|
|
|
29
30
|
/** Connections not saved since `before`, oldest first. Save activity is not necessarily refresh activity. */
|
|
30
31
|
listStale(before: Date): Promise<Connection[]>;
|
|
31
32
|
}
|
|
33
|
+
/** Anything with `query`: a `pg.Pool`, a `pg.PoolClient`, or a client inside the caller's open transaction. */
|
|
34
|
+
export interface PostgresQueryable {
|
|
35
|
+
query(sql: string, values?: unknown[]): Promise<QueryResult>;
|
|
36
|
+
}
|
|
37
|
+
/** The part of a person's runtime client the kit uses. `ConnectedCrouter` (`oauth.appClient(sub)`) satisfies it. */
|
|
38
|
+
export type ObjectRuntime = Pick<AppClient, 'customObjects' | 'canvas'>;
|
|
39
|
+
export interface ObjectKitTables {
|
|
40
|
+
/** One row per (person, kind, target): the registered custom object. Default `watch_object`. */
|
|
41
|
+
object?: string;
|
|
42
|
+
/** Nodes linked to an object. Default `watch_watcher`. */
|
|
43
|
+
watcher?: string;
|
|
44
|
+
/** The publish outbox. Default `watch_publish`. */
|
|
45
|
+
publish?: string;
|
|
46
|
+
}
|
|
47
|
+
/** The DDL the app migrates for the object kit. Table names follow `ObjectKitTables` (lowercase identifiers, optionally schema-qualified). */
|
|
48
|
+
export declare function objectKitSchemaSql(tables?: ObjectKitTables): string;
|
|
49
|
+
export interface PostgresObjectStoreOptions {
|
|
50
|
+
/** The app's provider id (`CROUTER_CLIENT_ID`). A kind `problem` registers as the custom-object kind `<providerId>.problem`, which the plugin's `objectKinds` declares. */
|
|
51
|
+
providerId: string;
|
|
52
|
+
/** The app's client on a person's runtime, e.g. `(sub) => oauth.appClient(sub)`. */
|
|
53
|
+
runtime(sub: string): Promise<ObjectRuntime>;
|
|
54
|
+
tables?: ObjectKitTables;
|
|
55
|
+
/** Receives retry and drop notices from the publisher. Default `console`. */
|
|
56
|
+
log?: {
|
|
57
|
+
warn(message: string): void;
|
|
58
|
+
error(message: string, error?: unknown): void;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export interface ObjectInstance {
|
|
62
|
+
objectId: string;
|
|
63
|
+
sub: string;
|
|
64
|
+
kind: string;
|
|
65
|
+
target: string;
|
|
66
|
+
name: string;
|
|
67
|
+
}
|
|
68
|
+
export interface PublishRequest {
|
|
69
|
+
event: string;
|
|
70
|
+
/** Truncated to 500 characters. */
|
|
71
|
+
summary: string;
|
|
72
|
+
state?: Record<string, unknown> | null;
|
|
73
|
+
}
|
|
74
|
+
export interface PublisherOptions {
|
|
75
|
+
/** Milliseconds between ticks. Default 5000. */
|
|
76
|
+
tick?: number;
|
|
77
|
+
/** Rows delivered per tick. Default 100. */
|
|
78
|
+
batch?: number;
|
|
79
|
+
}
|
|
80
|
+
export interface Publisher {
|
|
81
|
+
/** Stops the timer and waits for a tick in progress. */
|
|
82
|
+
stop(): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
/** A publish not delivered this long after it was enqueued is dropped. */
|
|
85
|
+
export declare const PUBLISH_WINDOW_MS: number;
|
|
86
|
+
/**
|
|
87
|
+
* Per-person custom objects on each person's hosted runtime, the nodes that watch them, and an at-least-once publish
|
|
88
|
+
* outbox. The app migrates the tables (`objectKitSchemaSql`), registers the kinds in its plugin, calls `instance` and
|
|
89
|
+
* `watch` from its commands, calls `enqueue` inside the transaction of the write an event records, and runs
|
|
90
|
+
* `startPublisher` in each process.
|
|
91
|
+
*/
|
|
92
|
+
export declare class PostgresObjectStore {
|
|
93
|
+
private readonly pool;
|
|
94
|
+
private readonly options;
|
|
95
|
+
private readonly object;
|
|
96
|
+
private readonly watcher;
|
|
97
|
+
private readonly publish;
|
|
98
|
+
private readonly lockKey;
|
|
99
|
+
private readonly log;
|
|
100
|
+
constructor(pool: PostgresPool, options: PostgresObjectStoreOptions);
|
|
101
|
+
/** The person's object for (kind, target), registered on their runtime the first time. A concurrent first call registers a second object; the row that committed first wins and the loser is unregistered. `target` is `''` for a kind with one object per person. */
|
|
102
|
+
instance(sub: string, kind: string, target: string, meta: {
|
|
103
|
+
name: string;
|
|
104
|
+
description: string;
|
|
105
|
+
}): Promise<ObjectInstance>;
|
|
106
|
+
/** Links `node` (on `machine`, `undefined` or `''` for the person's hosted runtime) to the object, then records the link. A node the runtime does not know rejects with `NotFoundError`. */
|
|
107
|
+
watch(objectId: string, node: string, machine?: string): Promise<void>;
|
|
108
|
+
/** Adds a publish row. Pass the client of the caller's open transaction so the row exists exactly when the write it records does. */
|
|
109
|
+
enqueue(client: PostgresQueryable, objectId: string, event: PublishRequest): Promise<void>;
|
|
110
|
+
/** Delivers due rows once under the advisory lock; false when another process holds it. */
|
|
111
|
+
tick(batch?: number): Promise<boolean>;
|
|
112
|
+
/** Runs `tick` every `tick` ms (default 5000, 100 rows). A failed tick is logged and the next one runs. */
|
|
113
|
+
startPublisher(options?: PublisherOptions): Publisher;
|
|
114
|
+
private find;
|
|
115
|
+
private deliver;
|
|
116
|
+
private end;
|
|
117
|
+
}
|
|
32
118
|
export {};
|
package/dist/stores/postgres.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { customObjectPublishFailure } from '@north-light/plugin';
|
|
4
|
+
import { APIError } from '../errors.js';
|
|
2
5
|
function tableName(name) {
|
|
3
6
|
const parts = name.split('.');
|
|
4
7
|
if (parts.length < 1 || parts.length > 2 || parts.some((part) => !/^[a-z_][a-z_0-9]*$/.test(part))) {
|
|
@@ -90,3 +93,182 @@ export class PostgresConnectionStore {
|
|
|
90
93
|
return result.rows.map(toConnection);
|
|
91
94
|
}
|
|
92
95
|
}
|
|
96
|
+
const DEFAULT_TABLES = { object: 'watch_object', watcher: 'watch_watcher', publish: 'watch_publish' };
|
|
97
|
+
/** The DDL the app migrates for the object kit. Table names follow `ObjectKitTables` (lowercase identifiers, optionally schema-qualified). */
|
|
98
|
+
export function objectKitSchemaSql(tables = {}) {
|
|
99
|
+
const t = { ...DEFAULT_TABLES, ...tables };
|
|
100
|
+
const [object, watcher, publish] = [t.object, t.watcher, t.publish].map(tableName);
|
|
101
|
+
const base = t.publish.split('.').pop();
|
|
102
|
+
return `CREATE TABLE ${object} (
|
|
103
|
+
object_id text PRIMARY KEY,
|
|
104
|
+
sub text NOT NULL,
|
|
105
|
+
kind text NOT NULL,
|
|
106
|
+
target text NOT NULL,
|
|
107
|
+
name text NOT NULL,
|
|
108
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
109
|
+
UNIQUE (sub, kind, target)
|
|
110
|
+
);
|
|
111
|
+
CREATE TABLE ${watcher} (
|
|
112
|
+
object_id text NOT NULL REFERENCES ${object} (object_id) ON DELETE CASCADE,
|
|
113
|
+
node text NOT NULL,
|
|
114
|
+
machine text NOT NULL DEFAULT '',
|
|
115
|
+
PRIMARY KEY (object_id, node, machine)
|
|
116
|
+
);
|
|
117
|
+
CREATE TABLE ${publish} (
|
|
118
|
+
id text PRIMARY KEY,
|
|
119
|
+
object_id text NOT NULL REFERENCES ${object} (object_id) ON DELETE CASCADE,
|
|
120
|
+
event text NOT NULL,
|
|
121
|
+
summary text NOT NULL,
|
|
122
|
+
state jsonb,
|
|
123
|
+
status text NOT NULL DEFAULT 'pending',
|
|
124
|
+
attempts integer NOT NULL DEFAULT 0,
|
|
125
|
+
last_error text,
|
|
126
|
+
next_attempt_at timestamptz NOT NULL DEFAULT now(),
|
|
127
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
128
|
+
);
|
|
129
|
+
CREATE INDEX "${base}_due_idx" ON ${publish} (status, next_attempt_at);
|
|
130
|
+
CREATE INDEX "${base}_object_idx" ON ${publish} (object_id);
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
/** A publish not delivered this long after it was enqueued is dropped. */
|
|
134
|
+
export const PUBLISH_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
135
|
+
const MAX_BACKOFF_MS = 15 * 60 * 1000;
|
|
136
|
+
/**
|
|
137
|
+
* Per-person custom objects on each person's hosted runtime, the nodes that watch them, and an at-least-once publish
|
|
138
|
+
* outbox. The app migrates the tables (`objectKitSchemaSql`), registers the kinds in its plugin, calls `instance` and
|
|
139
|
+
* `watch` from its commands, calls `enqueue` inside the transaction of the write an event records, and runs
|
|
140
|
+
* `startPublisher` in each process.
|
|
141
|
+
*/
|
|
142
|
+
export class PostgresObjectStore {
|
|
143
|
+
pool;
|
|
144
|
+
options;
|
|
145
|
+
object;
|
|
146
|
+
watcher;
|
|
147
|
+
publish;
|
|
148
|
+
lockKey;
|
|
149
|
+
log;
|
|
150
|
+
constructor(pool, options) {
|
|
151
|
+
this.pool = pool;
|
|
152
|
+
this.options = options;
|
|
153
|
+
const t = { ...DEFAULT_TABLES, ...options.tables };
|
|
154
|
+
this.object = tableName(t.object);
|
|
155
|
+
this.watcher = tableName(t.watcher);
|
|
156
|
+
this.publish = tableName(t.publish);
|
|
157
|
+
this.lockKey = `north-light-sdk-object-publish:${t.publish}`;
|
|
158
|
+
this.log = options.log ?? { warn: (m) => console.warn(m), error: (m, e) => console.error(m, e) };
|
|
159
|
+
}
|
|
160
|
+
/** The person's object for (kind, target), registered on their runtime the first time. A concurrent first call registers a second object; the row that committed first wins and the loser is unregistered. `target` is `''` for a kind with one object per person. */
|
|
161
|
+
async instance(sub, kind, target, meta) {
|
|
162
|
+
const existing = await this.find(sub, kind, target);
|
|
163
|
+
if (existing)
|
|
164
|
+
return existing;
|
|
165
|
+
const runtime = await this.options.runtime(sub);
|
|
166
|
+
const registered = await runtime.customObjects.register({
|
|
167
|
+
kind: `${this.options.providerId}.${kind}`,
|
|
168
|
+
name: meta.name,
|
|
169
|
+
description: meta.description,
|
|
170
|
+
privacy: 'private',
|
|
171
|
+
});
|
|
172
|
+
const inserted = await this.pool.query(`INSERT INTO ${this.object} (object_id, sub, kind, target, name) VALUES ($1, $2, $3, $4, $5)
|
|
173
|
+
ON CONFLICT (sub, kind, target) DO NOTHING RETURNING object_id`, [registered.id, sub, kind, target, meta.name]);
|
|
174
|
+
if (inserted.rows.length > 0)
|
|
175
|
+
return { objectId: registered.id, sub, kind, target, name: meta.name };
|
|
176
|
+
const winner = await this.find(sub, kind, target);
|
|
177
|
+
if (!winner)
|
|
178
|
+
throw new Error(`object ${sub}/${kind}/${target} vanished during registration`);
|
|
179
|
+
if (winner.objectId !== registered.id)
|
|
180
|
+
await runtime.customObjects.unregister(registered.id);
|
|
181
|
+
return winner;
|
|
182
|
+
}
|
|
183
|
+
/** Links `node` (on `machine`, `undefined` or `''` for the person's hosted runtime) to the object, then records the link. A node the runtime does not know rejects with `NotFoundError`. */
|
|
184
|
+
async watch(objectId, node, machine) {
|
|
185
|
+
const found = await this.pool.query(`SELECT sub FROM ${this.object} WHERE object_id = $1`, [objectId]);
|
|
186
|
+
const sub = found.rows[0]?.sub;
|
|
187
|
+
if (sub === undefined)
|
|
188
|
+
throw new Error(`no object ${objectId}`);
|
|
189
|
+
const runtime = await this.options.runtime(sub);
|
|
190
|
+
await runtime.canvas.watch(objectId, { watcher: node, ...(machine ? { watcher_machine: machine } : {}) });
|
|
191
|
+
await this.pool.query(`INSERT INTO ${this.watcher} (object_id, node, machine) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`, [objectId, node, machine ?? '']);
|
|
192
|
+
}
|
|
193
|
+
/** Adds a publish row. Pass the client of the caller's open transaction so the row exists exactly when the write it records does. */
|
|
194
|
+
async enqueue(client, objectId, event) {
|
|
195
|
+
await client.query(`INSERT INTO ${this.publish} (id, object_id, event, summary, state) VALUES ($1, $2, $3, $4, $5::jsonb)`, [randomUUID(), objectId, event.event, event.summary.slice(0, 500), event.state == null ? null : JSON.stringify(event.state)]);
|
|
196
|
+
}
|
|
197
|
+
/** Delivers due rows once under the advisory lock; false when another process holds it. */
|
|
198
|
+
async tick(batch = 100) {
|
|
199
|
+
const lock = await this.pool.connect();
|
|
200
|
+
try {
|
|
201
|
+
const got = await lock.query('SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS ok', [this.lockKey]);
|
|
202
|
+
if (!got.rows[0]?.ok)
|
|
203
|
+
return false;
|
|
204
|
+
try {
|
|
205
|
+
const due = await this.pool.query(`SELECT p.id, p.object_id, o.sub, p.event, p.summary, p.state, p.attempts, p.created_at
|
|
206
|
+
FROM ${this.publish} p JOIN ${this.object} o ON o.object_id = p.object_id
|
|
207
|
+
WHERE p.status = 'pending' AND p.next_attempt_at <= now()
|
|
208
|
+
ORDER BY p.created_at ASC, p.id ASC LIMIT $1`, [batch]);
|
|
209
|
+
for (const row of due.rows)
|
|
210
|
+
await this.deliver(row);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
await lock.query('SELECT pg_advisory_unlock(hashtextextended($1, 0))', [this.lockKey]);
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
lock.release();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Runs `tick` every `tick` ms (default 5000, 100 rows). A failed tick is logged and the next one runs. */
|
|
222
|
+
startPublisher(options = {}) {
|
|
223
|
+
let running;
|
|
224
|
+
const timer = setInterval(() => {
|
|
225
|
+
if (running)
|
|
226
|
+
return; // a pass is still delivering: skip this firing
|
|
227
|
+
running = this.tick(options.batch)
|
|
228
|
+
.catch((error) => this.log.error('object publish tick failed', error))
|
|
229
|
+
.finally(() => { running = undefined; });
|
|
230
|
+
}, options.tick ?? 5_000);
|
|
231
|
+
timer.unref();
|
|
232
|
+
return {
|
|
233
|
+
stop: async () => {
|
|
234
|
+
clearInterval(timer);
|
|
235
|
+
await running;
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async find(sub, kind, target) {
|
|
240
|
+
const result = await this.pool.query(`SELECT object_id, name FROM ${this.object} WHERE sub = $1 AND kind = $2 AND target = $3`, [sub, kind, target]);
|
|
241
|
+
const row = result.rows[0];
|
|
242
|
+
return row ? { objectId: row.object_id, sub, kind, target, name: row.name } : null;
|
|
243
|
+
}
|
|
244
|
+
async deliver(row) {
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
if (now - row.created_at.getTime() > PUBLISH_WINDOW_MS)
|
|
247
|
+
return this.end(row, 'dropped', '24 h window elapsed');
|
|
248
|
+
try {
|
|
249
|
+
const runtime = await this.options.runtime(row.sub);
|
|
250
|
+
await runtime.customObjects.publish(row.object_id, {
|
|
251
|
+
event: row.event,
|
|
252
|
+
summary: row.summary,
|
|
253
|
+
...(row.state == null ? {} : { state: row.state }),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
+
const refusal = error instanceof APIError ? { status: error.status, code: error.code } : undefined;
|
|
259
|
+
if (refusal && customObjectPublishFailure(refusal) === 'drop') {
|
|
260
|
+
return this.end(row, 'dropped', `${refusal.code}: ${message}`);
|
|
261
|
+
}
|
|
262
|
+
const attempts = row.attempts + 1;
|
|
263
|
+
await this.pool.query(`UPDATE ${this.publish} SET attempts = $2, last_error = $3, next_attempt_at = now() + $4 * interval '1 millisecond' WHERE id = $1`, [row.id, attempts, message.slice(0, 500), Math.min(2 ** attempts * 10_000, MAX_BACKOFF_MS)]);
|
|
264
|
+
this.log.warn(`publish ${row.id} attempt ${attempts} failed: ${message}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
await this.end(row, 'done', null);
|
|
268
|
+
}
|
|
269
|
+
async end(row, status, lastError) {
|
|
270
|
+
if (status === 'dropped')
|
|
271
|
+
this.log.warn(`dropped publish ${row.id}: ${lastError}`);
|
|
272
|
+
await this.pool.query(`UPDATE ${this.publish} SET status = $2, last_error = $3, attempts = attempts + 1 WHERE id = $1`, [row.id, status, lastError]);
|
|
273
|
+
}
|
|
274
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
|
|
2
|
-
export declare const SDK_VERSION = "0.3.
|
|
2
|
+
export declare const SDK_VERSION = "0.3.545";
|
|
3
3
|
/** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
|
|
4
4
|
* contract level existed (which only knows this header) refuses this SDK, while every later runtime accepts it. */
|
|
5
5
|
export declare const LEGACY_FLOOR = "0.3.423";
|
package/dist/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SDK_VERSION is written by scripts/release-version.mjs on every release; do not edit by hand.
|
|
2
2
|
/** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
|
|
3
|
-
export const SDK_VERSION = '0.3.
|
|
3
|
+
export const SDK_VERSION = '0.3.545';
|
|
4
4
|
// LEGACY_FLOOR is written once by `scripts/contract.mjs release`, at the first release that carries the contract level
|
|
5
5
|
// (the `0.0.0` placeholder is replaced there and never again); do not edit by hand.
|
|
6
6
|
/** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.545",
|
|
4
4
|
"crouterContract": "1.29",
|
|
5
5
|
"description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
|
|
6
6
|
"type": "module",
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
"prepublishOnly": "node -e \"if (process.env.npm_config_tag !== 'next') { console.error('@north-light/sdk publishes only with --tag next; scripts/promote-sdk.mjs is the only mover of latest.'); process.exit(1); }\""
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@north-light/api": "0.3.
|
|
48
|
-
"@north-light/identity": "0.3.
|
|
47
|
+
"@north-light/api": "0.3.545",
|
|
48
|
+
"@north-light/identity": "0.3.545",
|
|
49
|
+
"@north-light/plugin": "0.3.545",
|
|
49
50
|
"jose": "^6.2.1"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|