experimental-a2 0.10.0 → 0.12.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/CHANGELOG.md +45 -0
- package/dist/actor-DJi3RsNu.d.ts +310 -0
- package/dist/actor-DJi3RsNu.d.ts.map +1 -0
- package/dist/actor-client.d.ts +13 -4
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +63 -7
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -4
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +16 -2
- package/dist/actor-react.js.map +1 -1
- package/dist/actor.d.ts +2 -176
- package/dist/actor.js +13 -2
- package/dist/actor.js.map +1 -1
- package/dist/ai-server.d.ts +1 -1
- package/dist/ai-server.js +2 -2
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/client-Bf6uSEAk.js +1342 -0
- package/dist/client-Bf6uSEAk.js.map +1 -0
- package/dist/client-P_NNNRM-.d.ts +243 -0
- package/dist/client-P_NNNRM-.d.ts.map +1 -0
- package/dist/client.d.ts +2 -208
- package/dist/client.js +2 -1206
- package/dist/errors-DCk6ch5n.js.map +1 -1
- package/dist/errors-DvhSXnxk.d.ts +28 -0
- package/dist/errors-DvhSXnxk.d.ts.map +1 -0
- package/dist/index.d.ts +3 -35
- package/dist/{internal-DRXJ56EI.js → internal-Dq2qYxou.js} +2 -2
- package/dist/{internal-DRXJ56EI.js.map → internal-Dq2qYxou.js.map} +1 -1
- package/dist/platform-B4TnJtWu.js +34 -0
- package/dist/platform-B4TnJtWu.js.map +1 -0
- package/dist/react.d.ts +3 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -2
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-DlLyvaSH.js → server-BeNADlCI.js} +171 -65
- package/dist/server-BeNADlCI.js.map +1 -0
- package/dist/{server-DgCrSuhB.d.ts → server-DjZZa1wr.d.ts} +9 -3
- package/dist/{server-DgCrSuhB.d.ts.map → server-DjZZa1wr.d.ts.map} +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +2 -2
- package/dist/{store-DGHeBtIQ.d.ts → store-DtDOWLSn.d.ts} +4 -5
- package/dist/{store-DGHeBtIQ.d.ts.map → store-DtDOWLSn.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +1 -1
- package/dist/{store-polling-6DW7F1DT.js → store-polling-CmxUbV93.js} +56 -7
- package/dist/store-polling-CmxUbV93.js.map +1 -0
- package/dist/store-postgres.d.ts +6 -4
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +701 -53
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-C7-XZyW9.js +94 -0
- package/dist/store-presence-polling-C7-XZyW9.js.map +1 -0
- package/dist/store-redis-http.d.ts +2 -2
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +184 -38
- package/dist/store-redis-http.js.map +1 -1
- package/dist/{store-redis-core-z-ykbyMg.js → store-redis-notify-BUCyXOn0.js} +491 -27
- package/dist/store-redis-notify-BUCyXOn0.js.map +1 -0
- package/dist/store-redis.d.ts +5 -13
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +27 -271
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +1 -1
- package/dist/{wire--yji6mO3.js → wire-BO5wWCb1.js} +18 -16
- package/dist/wire-BO5wWCb1.js.map +1 -0
- package/docs/actors/04-routes.mdx +40 -8
- package/docs/guides/03-react.mdx +52 -10
- package/docs/guides/05-production.mdx +54 -5
- package/docs/guides/09-presence.mdx +12 -7
- package/docs/guides/10-transports.mdx +15 -2
- package/docs/reference/01-api.mdx +124 -17
- package/docs/reference/02-errors.mdx +45 -11
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/actor-client.ts +114 -14
- package/src/actor-react.ts +18 -3
- package/src/actor.ts +16 -1
- package/src/client-errors.ts +51 -0
- package/src/client.ts +253 -96
- package/src/errors.ts +4 -9
- package/src/index.ts +1 -1
- package/src/internal.ts +1 -1
- package/src/postgres-notification-scope.ts +134 -0
- package/src/postgres-notifications.ts +244 -0
- package/src/postgres-pool.ts +65 -0
- package/src/postgres-resources.ts +185 -0
- package/src/presence-recovery.ts +120 -0
- package/src/react.ts +3 -0
- package/src/redis-http-subscriptions.ts +199 -0
- package/src/server.ts +160 -22
- package/src/session-socket.ts +25 -7
- package/src/sse.ts +17 -0
- package/src/store-polling.ts +32 -12
- package/src/store-postgres.ts +292 -75
- package/src/store-presence-polling.ts +125 -0
- package/src/store-redis-core.ts +37 -30
- package/src/store-redis-http.ts +51 -45
- package/src/store-redis-notify.ts +464 -0
- package/src/store-redis.ts +9 -364
- package/src/store.ts +3 -4
- package/src/stream-activity.ts +32 -0
- package/src/wire.ts +39 -15
- package/dist/actor-shared-USo5MyuF.d.ts +0 -136
- package/dist/actor-shared-USo5MyuF.d.ts.map +0 -1
- package/dist/actor.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/server-DlLyvaSH.js.map +0 -1
- package/dist/store-polling-6DW7F1DT.js.map +0 -1
- package/dist/store-redis-core-z-ykbyMg.js.map +0 -1
- package/dist/wire--yji6mO3.js.map +0 -1
package/dist/store-postgres.js
CHANGED
|
@@ -1,9 +1,482 @@
|
|
|
1
1
|
import { t as A2Error } from "./errors-DCk6ch5n.js";
|
|
2
|
-
import {
|
|
2
|
+
import { h as nullProtoRecord, r as NOTIFY_TIMINGS } from "./internal-Dq2qYxou.js";
|
|
3
|
+
import { n as pollingStream, t as defaultSleep } from "./store-polling-CmxUbV93.js";
|
|
4
|
+
import { r as platformWaitUntil } from "./platform-B4TnJtWu.js";
|
|
3
5
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
4
6
|
import { t as idempotentReplay } from "./idempotent-replay-DVOlyYbx.js";
|
|
5
7
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
6
8
|
import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
|
|
9
|
+
import { t as pollPresence } from "./store-presence-polling-C7-XZyW9.js";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
//#region src/postgres-notifications.ts
|
|
12
|
+
const postgresNotificationChannel = ({ kind, sessionId }) => `a2_${kind}_${createHash("md5").update(sessionId).digest("hex")}`;
|
|
13
|
+
function postgresNotifications(createConnection) {
|
|
14
|
+
const channels = /* @__PURE__ */ new Map();
|
|
15
|
+
const ending = /* @__PURE__ */ new Set();
|
|
16
|
+
const end = (connection) => {
|
|
17
|
+
if (!connection) return;
|
|
18
|
+
const pending = connection.end().catch(() => {}).finally(() => {
|
|
19
|
+
ending.delete(pending);
|
|
20
|
+
});
|
|
21
|
+
ending.add(pending);
|
|
22
|
+
};
|
|
23
|
+
let current;
|
|
24
|
+
let task;
|
|
25
|
+
let retry;
|
|
26
|
+
let delay = 100;
|
|
27
|
+
let connectedAt = 0;
|
|
28
|
+
let closed = false;
|
|
29
|
+
const queues = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
const query = ({ connection, text }) => {
|
|
31
|
+
const result = (queues.get(connection) ?? Promise.resolve()).then(() => {
|
|
32
|
+
if (current !== connection) throw new Error("Postgres listener disconnected");
|
|
33
|
+
return connection.query(text);
|
|
34
|
+
});
|
|
35
|
+
queues.set(connection, result.catch(() => {}));
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
const fail = ({ connection, error }) => {
|
|
39
|
+
if (connection !== current) return;
|
|
40
|
+
current = void 0;
|
|
41
|
+
end(connection);
|
|
42
|
+
for (const entry of channels.values()) {
|
|
43
|
+
entry.listening = false;
|
|
44
|
+
for (const watcher of entry.watchers) watcher.reject(error);
|
|
45
|
+
}
|
|
46
|
+
if (closed || channels.size === 0 || retry) return;
|
|
47
|
+
if (Date.now() - connectedAt >= NOTIFY_TIMINGS.safetyReadMs) delay = 100;
|
|
48
|
+
retry = defaultSleep(delay);
|
|
49
|
+
delay = Math.min(delay * 2, NOTIFY_TIMINGS.safetyReadMs);
|
|
50
|
+
retry.promise.then(() => {
|
|
51
|
+
retry = void 0;
|
|
52
|
+
reconcile();
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const reconcile = () => {
|
|
56
|
+
if (closed || task || retry || channels.size === 0) return;
|
|
57
|
+
let attempt;
|
|
58
|
+
task = (async () => {
|
|
59
|
+
if (!current) {
|
|
60
|
+
connectedAt = Date.now();
|
|
61
|
+
const connection = await createConnection();
|
|
62
|
+
if (closed || channels.size === 0) {
|
|
63
|
+
await connection.end();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
current = connection;
|
|
67
|
+
attempt = connection;
|
|
68
|
+
connectedAt = Date.now();
|
|
69
|
+
connection.onNotification(({ channel, payload }) => {
|
|
70
|
+
if (current !== connection) return;
|
|
71
|
+
for (const watcher of channels.get(channel)?.watchers ?? []) watcher.notify(payload ?? "");
|
|
72
|
+
});
|
|
73
|
+
connection.onError((error) => fail({
|
|
74
|
+
connection,
|
|
75
|
+
error
|
|
76
|
+
}));
|
|
77
|
+
connection.onEnd(() => fail({
|
|
78
|
+
connection,
|
|
79
|
+
error: /* @__PURE__ */ new Error("Postgres listener disconnected")
|
|
80
|
+
}));
|
|
81
|
+
await connection.connect();
|
|
82
|
+
if (current !== connection) return;
|
|
83
|
+
}
|
|
84
|
+
const connection = current;
|
|
85
|
+
attempt = connection;
|
|
86
|
+
for (const [channel, entry] of channels) {
|
|
87
|
+
if (entry.listening) continue;
|
|
88
|
+
entry.listening = true;
|
|
89
|
+
entry.ready = query({
|
|
90
|
+
connection,
|
|
91
|
+
text: `LISTEN "${channel.replaceAll("\"", "\"\"")}"`
|
|
92
|
+
});
|
|
93
|
+
await entry.ready;
|
|
94
|
+
if (current !== connection) return;
|
|
95
|
+
for (const watcher of entry.watchers) {
|
|
96
|
+
watcher.resolve();
|
|
97
|
+
watcher.notify("");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})().catch((error) => {
|
|
101
|
+
if (attempt === current) fail({
|
|
102
|
+
connection: attempt,
|
|
103
|
+
error
|
|
104
|
+
});
|
|
105
|
+
}).finally(() => {
|
|
106
|
+
task = void 0;
|
|
107
|
+
if ([...channels.values()].some((entry) => !entry.listening)) reconcile();
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
watch({ channel, notify }) {
|
|
112
|
+
let resolve;
|
|
113
|
+
let reject;
|
|
114
|
+
const ready = new Promise((yes, no) => {
|
|
115
|
+
resolve = yes;
|
|
116
|
+
reject = no;
|
|
117
|
+
});
|
|
118
|
+
const watcher = {
|
|
119
|
+
notify,
|
|
120
|
+
resolve,
|
|
121
|
+
reject
|
|
122
|
+
};
|
|
123
|
+
let entry = channels.get(channel);
|
|
124
|
+
if (!entry) {
|
|
125
|
+
entry = {
|
|
126
|
+
listening: false,
|
|
127
|
+
watchers: /* @__PURE__ */ new Set()
|
|
128
|
+
};
|
|
129
|
+
channels.set(channel, entry);
|
|
130
|
+
}
|
|
131
|
+
entry.watchers.add(watcher);
|
|
132
|
+
if (entry.listening) entry.ready?.then(resolve, reject);
|
|
133
|
+
reconcile();
|
|
134
|
+
let stopped = false;
|
|
135
|
+
return {
|
|
136
|
+
ready,
|
|
137
|
+
stop() {
|
|
138
|
+
if (stopped) return;
|
|
139
|
+
stopped = true;
|
|
140
|
+
resolve();
|
|
141
|
+
entry.watchers.delete(watcher);
|
|
142
|
+
if (entry.watchers.size > 0) return;
|
|
143
|
+
channels.delete(channel);
|
|
144
|
+
if (channels.size === 0) {
|
|
145
|
+
retry?.cancel();
|
|
146
|
+
const connection = current;
|
|
147
|
+
current = void 0;
|
|
148
|
+
end(connection);
|
|
149
|
+
} else if (entry.listening) {
|
|
150
|
+
const connection = current;
|
|
151
|
+
if (connection) query({
|
|
152
|
+
connection,
|
|
153
|
+
text: `UNLISTEN "${channel.replaceAll("\"", "\"\"")}"`
|
|
154
|
+
}).catch((error) => fail({
|
|
155
|
+
connection,
|
|
156
|
+
error
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
async close() {
|
|
163
|
+
closed = true;
|
|
164
|
+
retry?.cancel();
|
|
165
|
+
for (const entry of channels.values()) for (const watcher of entry.watchers) watcher.resolve();
|
|
166
|
+
channels.clear();
|
|
167
|
+
const connection = current;
|
|
168
|
+
current = void 0;
|
|
169
|
+
end(connection);
|
|
170
|
+
await Promise.all(ending);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const POSTGRES_NOTIFICATIONS_SQL = `
|
|
175
|
+
create or replace function a2_notify_events() returns trigger language plpgsql as $$
|
|
176
|
+
begin
|
|
177
|
+
perform pg_notify('a2_events_' || md5(new.session_id), '');
|
|
178
|
+
return null;
|
|
179
|
+
end $$;
|
|
180
|
+
create or replace function a2_notify_presence() returns trigger language plpgsql as $$
|
|
181
|
+
begin
|
|
182
|
+
perform pg_notify('a2_presence_' || md5(case when TG_OP = 'DELETE' then old.ns else new.ns end),
|
|
183
|
+
coalesce(current_setting('a2.presence_source', true), ''));
|
|
184
|
+
return null;
|
|
185
|
+
end $$;
|
|
186
|
+
do $$ begin
|
|
187
|
+
if not exists (select 1 from pg_trigger where tgname = 'a2_notify_events' and tgrelid = 'a2_events'::regclass) then
|
|
188
|
+
create trigger a2_notify_events after insert on a2_events for each row execute function a2_notify_events();
|
|
189
|
+
end if;
|
|
190
|
+
if not exists (select 1 from pg_trigger where tgname = 'a2_notify_presence' and tgrelid = 'a2_presence'::regclass) then
|
|
191
|
+
create trigger a2_notify_presence after insert or update or delete on a2_presence for each row execute function a2_notify_presence();
|
|
192
|
+
end if;
|
|
193
|
+
end $$;
|
|
194
|
+
`;
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/postgres-notification-scope.ts
|
|
197
|
+
const failure = (error) => error instanceof A2Error ? error : new A2Error("STORE_UNAVAILABLE", "Postgres subscription failed", { cause: error });
|
|
198
|
+
function postgresNotificationScope(load) {
|
|
199
|
+
const state = {
|
|
200
|
+
shared: retryableLazy(load),
|
|
201
|
+
stops: /* @__PURE__ */ new Set(),
|
|
202
|
+
closed: false
|
|
203
|
+
};
|
|
204
|
+
const scope = {
|
|
205
|
+
watch(options) {
|
|
206
|
+
let stopped = false;
|
|
207
|
+
let cancel;
|
|
208
|
+
const cancelled = new Promise((resolve) => {
|
|
209
|
+
cancel = resolve;
|
|
210
|
+
});
|
|
211
|
+
let subscription;
|
|
212
|
+
const stop = () => {
|
|
213
|
+
stopped = true;
|
|
214
|
+
cancel();
|
|
215
|
+
subscription?.stop();
|
|
216
|
+
state.stops.delete(stop);
|
|
217
|
+
};
|
|
218
|
+
state.stops.add(stop);
|
|
219
|
+
return {
|
|
220
|
+
ready: Promise.race([cancelled, state.shared.get().then(async ({ value }) => {
|
|
221
|
+
if (stopped || state.closed) return;
|
|
222
|
+
subscription = value.watch(options);
|
|
223
|
+
await subscription.ready;
|
|
224
|
+
})]).catch((error) => {
|
|
225
|
+
if (!stopped && !state.closed) throw failure(error);
|
|
226
|
+
}),
|
|
227
|
+
stop
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
stream({ channel, read, startAfter }) {
|
|
231
|
+
return { [Symbol.asyncIterator]() {
|
|
232
|
+
let stopped = false;
|
|
233
|
+
let after = startAfter;
|
|
234
|
+
let buffer = [];
|
|
235
|
+
let version = 0;
|
|
236
|
+
let waiting;
|
|
237
|
+
let lease;
|
|
238
|
+
const stop = () => {
|
|
239
|
+
stopped = true;
|
|
240
|
+
waiting?.cancel();
|
|
241
|
+
lease?.stop();
|
|
242
|
+
state.stops.delete(stop);
|
|
243
|
+
};
|
|
244
|
+
state.stops.add(stop);
|
|
245
|
+
return {
|
|
246
|
+
async next() {
|
|
247
|
+
try {
|
|
248
|
+
if (stopped || state.closed) return {
|
|
249
|
+
done: true,
|
|
250
|
+
value: void 0
|
|
251
|
+
};
|
|
252
|
+
lease ??= scope.watch({
|
|
253
|
+
channel,
|
|
254
|
+
notify: () => {
|
|
255
|
+
version += 1;
|
|
256
|
+
waiting?.cancel();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
await lease.ready;
|
|
260
|
+
for (;;) {
|
|
261
|
+
if (stopped || state.closed) return {
|
|
262
|
+
done: true,
|
|
263
|
+
value: void 0
|
|
264
|
+
};
|
|
265
|
+
const row = buffer.shift();
|
|
266
|
+
if (row) {
|
|
267
|
+
after = row.index;
|
|
268
|
+
return {
|
|
269
|
+
done: false,
|
|
270
|
+
value: row
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const before = version;
|
|
274
|
+
buffer = await read(after);
|
|
275
|
+
if (stopped || state.closed) return {
|
|
276
|
+
done: true,
|
|
277
|
+
value: void 0
|
|
278
|
+
};
|
|
279
|
+
if (buffer.length || before !== version) continue;
|
|
280
|
+
waiting = defaultSleep(NOTIFY_TIMINGS.safetyReadMs);
|
|
281
|
+
await waiting.promise;
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (stopped || state.closed) return {
|
|
285
|
+
done: true,
|
|
286
|
+
value: void 0
|
|
287
|
+
};
|
|
288
|
+
stop();
|
|
289
|
+
throw failure(error);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
async return() {
|
|
293
|
+
stop();
|
|
294
|
+
return {
|
|
295
|
+
done: true,
|
|
296
|
+
value: void 0
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
} };
|
|
301
|
+
},
|
|
302
|
+
async close() {
|
|
303
|
+
if (state.closed) return;
|
|
304
|
+
state.closed = true;
|
|
305
|
+
for (const stop of state.stops) stop();
|
|
306
|
+
await (await state.shared.peek()?.catch(() => void 0))?.release();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
return scope;
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/postgres-pool.ts
|
|
313
|
+
function postgresPoolClient(pool) {
|
|
314
|
+
let timer;
|
|
315
|
+
let resolve;
|
|
316
|
+
const finish = () => {
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
timer = void 0;
|
|
319
|
+
resolve?.();
|
|
320
|
+
resolve = void 0;
|
|
321
|
+
};
|
|
322
|
+
const keepAlive = () => {
|
|
323
|
+
const idleTimeout = pool.options.idleTimeoutMillis;
|
|
324
|
+
if (pool.ending || pool.idleCount === 0 || typeof idleTimeout !== "number" || !Number.isFinite(idleTimeout) || idleTimeout <= 0) return;
|
|
325
|
+
finish();
|
|
326
|
+
const pending = new Promise((done) => {
|
|
327
|
+
resolve = done;
|
|
328
|
+
timer = setTimeout(finish, idleTimeout + 100);
|
|
329
|
+
timer.unref();
|
|
330
|
+
});
|
|
331
|
+
platformWaitUntil(pending);
|
|
332
|
+
};
|
|
333
|
+
pool.on("remove", () => {
|
|
334
|
+
if (pool.totalCount === 0) finish();
|
|
335
|
+
});
|
|
336
|
+
return {
|
|
337
|
+
async query(text, values) {
|
|
338
|
+
try {
|
|
339
|
+
return await pool.query(text, values);
|
|
340
|
+
} finally {
|
|
341
|
+
keepAlive();
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
async connect() {
|
|
345
|
+
const client = await pool.connect();
|
|
346
|
+
return {
|
|
347
|
+
query: (text, values) => client.query(text, values),
|
|
348
|
+
release() {
|
|
349
|
+
try {
|
|
350
|
+
client.release();
|
|
351
|
+
} finally {
|
|
352
|
+
keepAlive();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
async end() {
|
|
358
|
+
try {
|
|
359
|
+
await pool.end();
|
|
360
|
+
} finally {
|
|
361
|
+
finish();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/postgres-resources.ts
|
|
368
|
+
const registryKey = Symbol.for("experimental-a2.postgres-resources.v3");
|
|
369
|
+
const registry = () => {
|
|
370
|
+
let value = Reflect.get(globalThis, registryKey);
|
|
371
|
+
if (!value) {
|
|
372
|
+
value = {
|
|
373
|
+
pools: /* @__PURE__ */ new Map(),
|
|
374
|
+
injected: /* @__PURE__ */ new WeakMap(),
|
|
375
|
+
listeners: /* @__PURE__ */ new Map(),
|
|
376
|
+
identities: /* @__PURE__ */ new WeakMap(),
|
|
377
|
+
nextIdentity: 0
|
|
378
|
+
};
|
|
379
|
+
Reflect.set(globalThis, registryKey, value);
|
|
380
|
+
}
|
|
381
|
+
return value;
|
|
382
|
+
};
|
|
383
|
+
const acquire = ({ cache, key, create }) => {
|
|
384
|
+
let entry = cache.get(key);
|
|
385
|
+
if (!entry) {
|
|
386
|
+
entry = {
|
|
387
|
+
value: create(),
|
|
388
|
+
refs: 0
|
|
389
|
+
};
|
|
390
|
+
cache.set(key, entry);
|
|
391
|
+
}
|
|
392
|
+
entry.refs += 1;
|
|
393
|
+
const held = entry;
|
|
394
|
+
let released = false;
|
|
395
|
+
return {
|
|
396
|
+
value: entry.value,
|
|
397
|
+
async release() {
|
|
398
|
+
if (released) return;
|
|
399
|
+
released = true;
|
|
400
|
+
held.refs -= 1;
|
|
401
|
+
if (held.refs !== 0) return;
|
|
402
|
+
if (cache.get(key) === held) cache.delete(key);
|
|
403
|
+
await held.value.close();
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
};
|
|
407
|
+
function postgresConnectionKey({ connectionString, defaults }) {
|
|
408
|
+
const shared = registry();
|
|
409
|
+
const visited = /* @__PURE__ */ new Set();
|
|
410
|
+
const identity = (value) => {
|
|
411
|
+
let id = shared.identities.get(value);
|
|
412
|
+
if (id === void 0) {
|
|
413
|
+
id = ++shared.nextIdentity;
|
|
414
|
+
shared.identities.set(value, id);
|
|
415
|
+
}
|
|
416
|
+
return id;
|
|
417
|
+
};
|
|
418
|
+
const encode = (value) => {
|
|
419
|
+
if (typeof value === "function") return ["reference", identity(value)];
|
|
420
|
+
if (value === null || typeof value !== "object") return [typeof value, String(value)];
|
|
421
|
+
if (Buffer.isBuffer(value)) return ["buffer", value.toString("base64")];
|
|
422
|
+
const prototype = Object.getPrototypeOf(value);
|
|
423
|
+
if (visited.has(value) || prototype !== Object.prototype && prototype !== null && !Array.isArray(value)) return ["reference", identity(value)];
|
|
424
|
+
visited.add(value);
|
|
425
|
+
const result = [Array.isArray(value) ? "array" : "object", Object.getOwnPropertyNames(value).toSorted().map((key) => [key, encode(Reflect.get(value, key))])];
|
|
426
|
+
visited.delete(value);
|
|
427
|
+
return result;
|
|
428
|
+
};
|
|
429
|
+
const environment = Object.keys(process.env).filter((key) => key.startsWith("PG")).toSorted().map((key) => [key, process.env[key]]);
|
|
430
|
+
return createHash("sha256").update(JSON.stringify([
|
|
431
|
+
connectionString,
|
|
432
|
+
environment,
|
|
433
|
+
encode(defaults)
|
|
434
|
+
])).digest("hex");
|
|
435
|
+
}
|
|
436
|
+
const queries = (raw) => {
|
|
437
|
+
let pending = Promise.resolve();
|
|
438
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
439
|
+
const exclusive = (fn) => {
|
|
440
|
+
const result = pending.then(fn);
|
|
441
|
+
pending = result.catch(() => {});
|
|
442
|
+
return result;
|
|
443
|
+
};
|
|
444
|
+
return {
|
|
445
|
+
raw,
|
|
446
|
+
client: raw.connect ? raw : { query: (text, values) => exclusive(() => raw.query(text, values)) },
|
|
447
|
+
exclusive,
|
|
448
|
+
initialize({ schema, run }) {
|
|
449
|
+
const existing = schemas.get(schema);
|
|
450
|
+
if (existing) return existing;
|
|
451
|
+
const attempt = run();
|
|
452
|
+
schemas.set(schema, attempt);
|
|
453
|
+
attempt.catch(() => {
|
|
454
|
+
if (schemas.get(schema) === attempt) schemas.delete(schema);
|
|
455
|
+
});
|
|
456
|
+
return attempt;
|
|
457
|
+
},
|
|
458
|
+
async close() {
|
|
459
|
+
await pending;
|
|
460
|
+
await raw.end?.();
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
const sharePostgresPool = ({ key, create }) => acquire({
|
|
465
|
+
cache: registry().pools,
|
|
466
|
+
key,
|
|
467
|
+
create: () => queries(create())
|
|
468
|
+
});
|
|
469
|
+
const sharePostgresClient = (client) => acquire({
|
|
470
|
+
cache: registry().injected,
|
|
471
|
+
key: client,
|
|
472
|
+
create: () => queries(client)
|
|
473
|
+
});
|
|
474
|
+
const sharePostgresListener = ({ key, create }) => acquire({
|
|
475
|
+
cache: registry().listeners,
|
|
476
|
+
key,
|
|
477
|
+
create
|
|
478
|
+
});
|
|
479
|
+
//#endregion
|
|
7
480
|
//#region src/store-postgres.ts
|
|
8
481
|
/**
|
|
9
482
|
* experimental-a2/store-postgres — postgres store backend (the production path).
|
|
@@ -17,9 +490,8 @@ import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
|
|
|
17
490
|
*
|
|
18
491
|
* Semantics mirror the sqlite backend: `index` is stored as `idx`,
|
|
19
492
|
* timestamps are epoch milliseconds written from the injected clock —
|
|
20
|
-
* never SQL `now()
|
|
21
|
-
*
|
|
22
|
-
* on the transport.) Appends serialize per session via
|
|
493
|
+
* never SQL `now()`. Streams poll by default; an optional direct
|
|
494
|
+
* connection enables LISTEN/NOTIFY for events and presence. Appends serialize per session via
|
|
23
495
|
* `pg_advisory_xact_lock`, so concurrent writers can't race the index
|
|
24
496
|
* assignment.
|
|
25
497
|
*/
|
|
@@ -155,16 +627,17 @@ const wrap = async (fn) => {
|
|
|
155
627
|
}
|
|
156
628
|
};
|
|
157
629
|
const jsonb = (value) => JSON.stringify(value) ?? "null";
|
|
158
|
-
const initializeSchema = async (client) => {
|
|
630
|
+
const initializeSchema = async ({ client, listen }) => {
|
|
631
|
+
const schema = SCHEMA + (listen ? POSTGRES_NOTIFICATIONS_SQL : "");
|
|
159
632
|
if (!client.connect) {
|
|
160
|
-
await client.query(
|
|
633
|
+
await client.query(schema);
|
|
161
634
|
return;
|
|
162
635
|
}
|
|
163
636
|
const conn = await client.connect();
|
|
164
637
|
try {
|
|
165
638
|
await conn.query("begin");
|
|
166
639
|
await conn.query("select pg_advisory_xact_lock(hashtext('a2:postgres:schema'))");
|
|
167
|
-
await conn.query(
|
|
640
|
+
await conn.query(schema);
|
|
168
641
|
await conn.query("commit");
|
|
169
642
|
} catch (err) {
|
|
170
643
|
await conn.query("rollback").catch(() => {});
|
|
@@ -173,28 +646,99 @@ const initializeSchema = async (client) => {
|
|
|
173
646
|
conn.release();
|
|
174
647
|
}
|
|
175
648
|
};
|
|
649
|
+
const loadPg = async () => import("pg").catch(() => {
|
|
650
|
+
throw new A2Error("STORE_NOT_CONFIGURED", "Postgres connections created by A2 require the optional 'pg' package");
|
|
651
|
+
});
|
|
652
|
+
const createListener = ({ pg, connectionString }) => postgresNotifications(async () => {
|
|
653
|
+
const listener = new pg.default.Client({
|
|
654
|
+
connectionString,
|
|
655
|
+
application_name: "a2-listener",
|
|
656
|
+
connectionTimeoutMillis: 1e4
|
|
657
|
+
});
|
|
658
|
+
return {
|
|
659
|
+
connect: () => listener.connect(),
|
|
660
|
+
query: (text) => listener.query(text),
|
|
661
|
+
end: () => listener.end(),
|
|
662
|
+
onNotification: (callback) => {
|
|
663
|
+
listener.on("notification", callback);
|
|
664
|
+
},
|
|
665
|
+
onError: (callback) => {
|
|
666
|
+
listener.on("error", callback);
|
|
667
|
+
},
|
|
668
|
+
onEnd: (callback) => {
|
|
669
|
+
listener.on("end", callback);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
});
|
|
176
673
|
function postgres(options = {}) {
|
|
177
674
|
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
178
675
|
const generateId = options.ids ?? RANDOM_IDS;
|
|
676
|
+
const instanceId = crypto.randomUUID();
|
|
677
|
+
let closed = false;
|
|
678
|
+
if (options.listenConnectionString === "") throw new TypeError("listenConnectionString must be a direct Postgres connection string");
|
|
179
679
|
if (!options.client && !options.connectionString) throw new TypeError("postgres() needs a connectionString or an injected client");
|
|
680
|
+
const injected = options.client ? sharePostgresClient(options.client) : void 0;
|
|
180
681
|
const connection = retryableLazy(async () => {
|
|
181
|
-
let
|
|
682
|
+
let lease = injected;
|
|
683
|
+
if (!lease) {
|
|
684
|
+
const pg = await loadPg();
|
|
685
|
+
const connectionString = options.connectionString;
|
|
686
|
+
const key = postgresConnectionKey({
|
|
687
|
+
connectionString,
|
|
688
|
+
defaults: pg.default.defaults
|
|
689
|
+
});
|
|
690
|
+
lease = sharePostgresPool({
|
|
691
|
+
key,
|
|
692
|
+
create: () => {
|
|
693
|
+
const pool = new pg.default.Pool({ connectionString });
|
|
694
|
+
pool.on("error", () => {});
|
|
695
|
+
return postgresPoolClient(pool);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
182
699
|
try {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
700
|
+
const listen = Boolean(options.listenConnectionString);
|
|
701
|
+
const resource = lease.value;
|
|
702
|
+
if (injected) await initializeSchema({
|
|
703
|
+
client: resource.client,
|
|
704
|
+
listen
|
|
705
|
+
});
|
|
706
|
+
else await resource.initialize({
|
|
707
|
+
schema: SCHEMA + (listen ? POSTGRES_NOTIFICATIONS_SQL : ""),
|
|
708
|
+
run: () => initializeSchema({
|
|
709
|
+
client: resource.client,
|
|
710
|
+
listen
|
|
711
|
+
})
|
|
712
|
+
});
|
|
713
|
+
return lease;
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (!injected) await lease.release().catch(() => {});
|
|
716
|
+
throw error;
|
|
195
717
|
}
|
|
196
718
|
});
|
|
197
|
-
const client =
|
|
719
|
+
const client = async () => {
|
|
720
|
+
if (closed) throw new A2Error("STORE_UNAVAILABLE", "the store was closed");
|
|
721
|
+
const lease = await connection.get();
|
|
722
|
+
if (closed) throw new A2Error("STORE_UNAVAILABLE", "the store was closed");
|
|
723
|
+
return lease.value.client;
|
|
724
|
+
};
|
|
725
|
+
const notifications = options.listenConnectionString ? postgresNotificationScope(async () => {
|
|
726
|
+
await client();
|
|
727
|
+
const pg = await loadPg();
|
|
728
|
+
const connectionString = options.listenConnectionString;
|
|
729
|
+
const key = postgresConnectionKey({
|
|
730
|
+
connectionString,
|
|
731
|
+
defaults: pg.default.defaults
|
|
732
|
+
});
|
|
733
|
+
return sharePostgresListener({
|
|
734
|
+
key,
|
|
735
|
+
create: () => createListener({
|
|
736
|
+
pg,
|
|
737
|
+
connectionString
|
|
738
|
+
})
|
|
739
|
+
});
|
|
740
|
+
}) : void 0;
|
|
741
|
+
const presenceSubscriptions = /* @__PURE__ */ new Map();
|
|
198
742
|
const readStateBatch = async (requests, reducerName) => {
|
|
199
743
|
if (requests.length === 0) return [];
|
|
200
744
|
const c = await client();
|
|
@@ -279,13 +823,13 @@ function postgres(options = {}) {
|
|
|
279
823
|
}
|
|
280
824
|
return reads;
|
|
281
825
|
};
|
|
282
|
-
|
|
826
|
+
const begin = notifications ? `begin; set local a2.presence_source = '${instanceId}'` : "begin";
|
|
283
827
|
const withTx = async (fn) => {
|
|
284
828
|
const c = await client();
|
|
285
829
|
if (c.connect) {
|
|
286
830
|
const conn = await c.connect();
|
|
287
831
|
try {
|
|
288
|
-
await conn.query(
|
|
832
|
+
await conn.query(begin);
|
|
289
833
|
const result = await fn((text, values) => conn.query(text, values));
|
|
290
834
|
await conn.query("commit");
|
|
291
835
|
return result;
|
|
@@ -296,21 +840,21 @@ function postgres(options = {}) {
|
|
|
296
840
|
conn.release();
|
|
297
841
|
}
|
|
298
842
|
}
|
|
299
|
-
const
|
|
300
|
-
|
|
843
|
+
const resource = (await connection.get()).value;
|
|
844
|
+
return resource.exclusive(async () => {
|
|
845
|
+
const raw = resource.raw;
|
|
846
|
+
await raw.query(begin);
|
|
301
847
|
try {
|
|
302
|
-
const result = await fn((text, values) =>
|
|
303
|
-
await
|
|
848
|
+
const result = await fn((text, values) => raw.query(text, values));
|
|
849
|
+
await raw.query("commit");
|
|
304
850
|
return result;
|
|
305
851
|
} catch (err) {
|
|
306
|
-
await
|
|
852
|
+
await raw.query("rollback").catch(() => {});
|
|
307
853
|
throw err;
|
|
308
854
|
}
|
|
309
855
|
});
|
|
310
|
-
mutex = run.catch(() => {});
|
|
311
|
-
return run;
|
|
312
856
|
};
|
|
313
|
-
|
|
857
|
+
const store = {
|
|
314
858
|
async append(sessionId, events) {
|
|
315
859
|
if (events.length === 0) return wrap(async () => {
|
|
316
860
|
const { rows } = await (await client()).query(`select exists(
|
|
@@ -838,17 +1382,18 @@ function postgres(options = {}) {
|
|
|
838
1382
|
const entries = Object.entries(values);
|
|
839
1383
|
if (entries.length === 0) return;
|
|
840
1384
|
await wrap(async () => {
|
|
841
|
-
const c = await client();
|
|
842
1385
|
const atMs = meta.at.getTime();
|
|
843
1386
|
const expiresAtMs = clock.now().getTime() + meta.ttlMs;
|
|
844
|
-
|
|
1387
|
+
const applied = nullProtoRecord();
|
|
1388
|
+
const write = async (query) => {
|
|
1389
|
+
for (const [field, value] of entries) if ((await (value === null ? query(`delete from a2_presence
|
|
845
1390
|
where ns = $1 and participant = $2 and field = $3
|
|
846
|
-
and at <= $4`, [
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1391
|
+
and at <= $4${notifications ? " returning field" : ""}`, [
|
|
1392
|
+
ns,
|
|
1393
|
+
participant,
|
|
1394
|
+
field,
|
|
1395
|
+
atMs
|
|
1396
|
+
]) : query(`insert into a2_presence
|
|
852
1397
|
(ns, participant, field, value, seen, at, expires_at)
|
|
853
1398
|
values ($1, $2, $3, $4::jsonb, $5, $6, $7)
|
|
854
1399
|
on conflict (ns, participant, field) do update set
|
|
@@ -856,15 +1401,32 @@ function postgres(options = {}) {
|
|
|
856
1401
|
seen = excluded.seen,
|
|
857
1402
|
at = excluded.at,
|
|
858
1403
|
expires_at = excluded.expires_at
|
|
859
|
-
where a2_presence.at <= excluded.at`, [
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1404
|
+
where a2_presence.at <= excluded.at${notifications ? " returning field" : ""}`, [
|
|
1405
|
+
ns,
|
|
1406
|
+
participant,
|
|
1407
|
+
field,
|
|
1408
|
+
jsonb(value),
|
|
1409
|
+
meta.seen,
|
|
1410
|
+
atMs,
|
|
1411
|
+
expiresAtMs
|
|
1412
|
+
]))).rows.length) applied[field] = structuredClone(value);
|
|
1413
|
+
};
|
|
1414
|
+
if (notifications) await withTx(write);
|
|
1415
|
+
else {
|
|
1416
|
+
const c = await client();
|
|
1417
|
+
await write((text, parameters) => c.query(text, parameters));
|
|
1418
|
+
}
|
|
1419
|
+
if (Object.keys(applied).length) {
|
|
1420
|
+
const patch = {
|
|
1421
|
+
participant,
|
|
1422
|
+
values: applied,
|
|
1423
|
+
seen: meta.seen,
|
|
1424
|
+
at: new Date(atMs)
|
|
1425
|
+
};
|
|
1426
|
+
const subscription = presenceSubscriptions.get(ns);
|
|
1427
|
+
subscription?.poll.update(patch);
|
|
1428
|
+
for (const listener of subscription?.listeners ?? []) listener(patch);
|
|
1429
|
+
}
|
|
868
1430
|
});
|
|
869
1431
|
},
|
|
870
1432
|
async read(ns) {
|
|
@@ -892,17 +1454,103 @@ function postgres(options = {}) {
|
|
|
892
1454
|
},
|
|
893
1455
|
stream(sessionId, opts) {
|
|
894
1456
|
const readAfter = (afterIndex) => wrap(async () => {
|
|
895
|
-
const { rows } = await (await client()).query(
|
|
896
|
-
|
|
1457
|
+
const { rows } = await (await client()).query(`select session_id, idx, event_type, payload, event_id, created_at
|
|
1458
|
+
from a2_events
|
|
1459
|
+
where session_id = $1 and idx > $2
|
|
1460
|
+
order by idx`, [sessionId, afterIndex]);
|
|
1461
|
+
return rows.map(toEvent);
|
|
1462
|
+
});
|
|
1463
|
+
if (notifications) return notifications.stream({
|
|
1464
|
+
channel: postgresNotificationChannel({
|
|
1465
|
+
kind: "events",
|
|
1466
|
+
sessionId
|
|
1467
|
+
}),
|
|
1468
|
+
read: readAfter,
|
|
1469
|
+
startAfter: opts?.startAfter ?? 0
|
|
897
1470
|
});
|
|
898
1471
|
return pollingStream(readAfter, opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
|
|
899
1472
|
},
|
|
900
1473
|
async close() {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1474
|
+
if (closed) return;
|
|
1475
|
+
closed = true;
|
|
1476
|
+
for (const subscription of presenceSubscriptions.values()) subscription.stop();
|
|
1477
|
+
presenceSubscriptions.clear();
|
|
1478
|
+
await notifications?.close();
|
|
1479
|
+
const lease = await connection.peek()?.catch(() => void 0);
|
|
1480
|
+
await (injected ?? lease)?.release();
|
|
904
1481
|
}
|
|
905
1482
|
};
|
|
1483
|
+
if (notifications) {
|
|
1484
|
+
const presence = store.presence;
|
|
1485
|
+
const read = presence.read;
|
|
1486
|
+
presence.read = async (ns) => {
|
|
1487
|
+
const subscription = presenceSubscriptions.get(ns);
|
|
1488
|
+
await subscription?.ready;
|
|
1489
|
+
await subscription?.poll.ready;
|
|
1490
|
+
return read(ns);
|
|
1491
|
+
};
|
|
1492
|
+
presence.subscribe = (ns, onPatch) => {
|
|
1493
|
+
if (closed) throw new A2Error("STORE_UNAVAILABLE", "the store was closed");
|
|
1494
|
+
let subscription = presenceSubscriptions.get(ns);
|
|
1495
|
+
if (!subscription) {
|
|
1496
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1497
|
+
let dirty = false;
|
|
1498
|
+
let ended = false;
|
|
1499
|
+
let waiting;
|
|
1500
|
+
const lease = notifications.watch({
|
|
1501
|
+
channel: postgresNotificationChannel({
|
|
1502
|
+
kind: "presence",
|
|
1503
|
+
sessionId: ns
|
|
1504
|
+
}),
|
|
1505
|
+
notify: (source) => {
|
|
1506
|
+
if (source === instanceId) return;
|
|
1507
|
+
dirty = true;
|
|
1508
|
+
waiting?.cancel();
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
const poll = pollPresence({
|
|
1512
|
+
read: async () => {
|
|
1513
|
+
await lease.ready;
|
|
1514
|
+
if (ended || closed) return [];
|
|
1515
|
+
dirty = false;
|
|
1516
|
+
return read(ns);
|
|
1517
|
+
},
|
|
1518
|
+
deliver: (patch) => {
|
|
1519
|
+
for (const listener of listeners) listener(patch);
|
|
1520
|
+
},
|
|
1521
|
+
sleep: () => {
|
|
1522
|
+
waiting = defaultSleep(dirty ? 0 : NOTIFY_TIMINGS.safetyReadMs);
|
|
1523
|
+
return waiting;
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
lease.ready.catch(() => poll.stop());
|
|
1527
|
+
subscription = {
|
|
1528
|
+
listeners,
|
|
1529
|
+
ready: lease.ready,
|
|
1530
|
+
poll,
|
|
1531
|
+
stop() {
|
|
1532
|
+
ended = true;
|
|
1533
|
+
poll.stop();
|
|
1534
|
+
lease.stop();
|
|
1535
|
+
waiting?.cancel();
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
presenceSubscriptions.set(ns, subscription);
|
|
1539
|
+
}
|
|
1540
|
+
subscription.listeners.add(onPatch);
|
|
1541
|
+
let stopped = false;
|
|
1542
|
+
return () => {
|
|
1543
|
+
if (stopped) return;
|
|
1544
|
+
stopped = true;
|
|
1545
|
+
subscription.listeners.delete(onPatch);
|
|
1546
|
+
if (subscription.listeners.size === 0) {
|
|
1547
|
+
presenceSubscriptions.delete(ns);
|
|
1548
|
+
subscription.stop();
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
return store;
|
|
906
1554
|
}
|
|
907
1555
|
//#endregion
|
|
908
1556
|
export { postgres };
|