@dbos-inc/dbos-sdk 4.27.4-preview → 4.27.6
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/dist/src/client.d.ts +12 -1
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +28 -52
- package/dist/src/client.js.map +1 -1
- package/dist/src/context.d.ts +2 -0
- package/dist/src/context.d.ts.map +1 -1
- package/dist/src/context.js +10 -4
- package/dist/src/context.js.map +1 -1
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +1 -0
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos.d.ts +29 -2
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +61 -60
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/error.d.ts +22 -0
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +48 -1
- package/dist/src/error.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/streams.d.ts +17 -0
- package/dist/src/streams.d.ts.map +1 -0
- package/dist/src/streams.js +247 -0
- package/dist/src/streams.js.map +1 -0
- package/dist/src/system_database.d.ts +20 -1
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +96 -60
- package/dist/src/system_database.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.SystemDatabase = exports.verifySystemDatabase = exports.ensureSystemDatabase = exports.grantDbosSchemaPermissions = exports.getDbosSchemaPermissionsSql = exports.DEFAULT_RENAME_BATCH_SIZE = exports.DEFAULT_NOTIFICATION_COALESCE_MS = exports.DBOS_STREAMS_CHANNEL = exports.DBOS_WORKFLOW_EVENTS_CHANNEL = exports.DBOS_NOTIFICATIONS_CHANNEL = exports.DBOS_STREAM_CLOSED_SENTINEL = exports.DEFAULT_POOL_SIZE = exports.DBOS_FUNCNAME_CLOSESTREAM = exports.DBOS_FUNCNAME_WRITESTREAM = exports.DBOS_FUNCNAME_GETSTATUS = exports.DBOS_FUNCNAME_SLEEP = exports.DBOS_FUNCNAME_GETEVENT = exports.DBOS_FUNCNAME_SETEVENT = exports.DBOS_FUNCNAME_RECV = exports.DBOS_FUNCNAME_SEND = void 0;
|
|
12
|
+
exports.SystemDatabase = exports.verifySystemDatabase = exports.ensureSystemDatabase = exports.grantDbosSchemaPermissions = exports.getDbosSchemaPermissionsSql = exports.DEFAULT_RENAME_BATCH_SIZE = exports.DEFAULT_NOTIFICATION_COALESCE_MS = exports.DBOS_STREAMS_CHANNEL = exports.DBOS_WORKFLOW_EVENTS_CHANNEL = exports.DBOS_NOTIFICATIONS_CHANNEL = exports.isLegacyClosedSentinel = exports.isStreamClosedSentinel = exports.DBOS_STREAM_CLOSED_SENTINEL_SERIALIZED = exports.DBOS_STREAM_CLOSED_SENTINEL = exports.DEFAULT_POOL_SIZE = exports.DBOS_FUNCNAME_READSTREAMOFFSET = exports.DBOS_FUNCNAME_READSTREAM = exports.DBOS_FUNCNAME_CLOSESTREAM = exports.DBOS_FUNCNAME_WRITESTREAM = exports.DBOS_FUNCNAME_GETSTATUS = exports.DBOS_FUNCNAME_SLEEP = exports.DBOS_FUNCNAME_GETEVENT = exports.DBOS_FUNCNAME_SETEVENT = exports.DBOS_FUNCNAME_RECV = exports.DBOS_FUNCNAME_SEND = void 0;
|
|
13
13
|
const dbos_executor_1 = require("./dbos-executor");
|
|
14
14
|
const pg_1 = require("pg");
|
|
15
15
|
const error_1 = require("./error");
|
|
@@ -31,8 +31,25 @@ exports.DBOS_FUNCNAME_SLEEP = 'DBOS.sleep';
|
|
|
31
31
|
exports.DBOS_FUNCNAME_GETSTATUS = 'getStatus';
|
|
32
32
|
exports.DBOS_FUNCNAME_WRITESTREAM = 'DBOS.writeStream';
|
|
33
33
|
exports.DBOS_FUNCNAME_CLOSESTREAM = 'DBOS.closeStream';
|
|
34
|
+
exports.DBOS_FUNCNAME_READSTREAM = 'DBOS.readStream';
|
|
35
|
+
exports.DBOS_FUNCNAME_READSTREAMOFFSET = 'DBOS.readStreamOffset';
|
|
34
36
|
exports.DEFAULT_POOL_SIZE = 10;
|
|
35
37
|
exports.DBOS_STREAM_CLOSED_SENTINEL = '__DBOS_STREAM_CLOSED__';
|
|
38
|
+
// The sentinel as it is stored: portable JSON, the same bytes every language writes and reads.
|
|
39
|
+
exports.DBOS_STREAM_CLOSED_SENTINEL_SERIALIZED = serialization_1.DBOSPortableJSON.stringify(exports.DBOS_STREAM_CLOSED_SENTINEL);
|
|
40
|
+
/** Whether a stream value is the marker a closed stream ends with. Takes the deserialized value. */
|
|
41
|
+
function isStreamClosedSentinel(value) {
|
|
42
|
+
return typeof value === 'string' && value === exports.DBOS_STREAM_CLOSED_SENTINEL;
|
|
43
|
+
}
|
|
44
|
+
exports.isStreamClosedSentinel = isStreamClosedSentinel;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a stored value is the marker as releases before the portable form wrote it: unserialized,
|
|
47
|
+
* so no deserializer parses it. Callers must test this before deserializing.
|
|
48
|
+
*/
|
|
49
|
+
function isLegacyClosedSentinel(serializedValue) {
|
|
50
|
+
return serializedValue === exports.DBOS_STREAM_CLOSED_SENTINEL;
|
|
51
|
+
}
|
|
52
|
+
exports.isLegacyClosedSentinel = isLegacyClosedSentinel;
|
|
36
53
|
// LISTEN/NOTIFY channels. Streams and workflow_events are pushed by the notifier loop off the write path; notifications fires from an in-transaction DB trigger so recv is never woken before its row commits.
|
|
37
54
|
exports.DBOS_NOTIFICATIONS_CHANNEL = 'dbos_notifications_channel';
|
|
38
55
|
exports.DBOS_WORKFLOW_EVENTS_CHANNEL = 'dbos_workflow_events_channel';
|
|
@@ -474,6 +491,7 @@ class SystemDatabase {
|
|
|
474
491
|
notificationsClient = null;
|
|
475
492
|
dbPollingIntervalResultMs = 1000;
|
|
476
493
|
dbPollingIntervalEventMs = 10000;
|
|
494
|
+
dbPollingIntervalStreamMs = 1000;
|
|
477
495
|
shouldUseDBNotifications = true;
|
|
478
496
|
notificationsMap = new NotificationMap();
|
|
479
497
|
workflowEventsMap = new NotificationMap();
|
|
@@ -1722,7 +1740,8 @@ class SystemDatabase {
|
|
|
1722
1740
|
* under the polling limiter so it counts against the same concurrency budget
|
|
1723
1741
|
* as the rest of the loop's reads.
|
|
1724
1742
|
*/
|
|
1725
|
-
|
|
1743
|
+
/** Cancellation check for polling waits: goes through the limiter so readers cannot starve the pool. */
|
|
1744
|
+
async checkIfCanceledLimited(workflowID) {
|
|
1726
1745
|
await this.#pollWithLimiter(() => this.#checkIfCanceled(this.pool, workflowID));
|
|
1727
1746
|
}
|
|
1728
1747
|
// A missing row normally means the workflow has not been inserted yet, so
|
|
@@ -1741,7 +1760,7 @@ class SystemDatabase {
|
|
|
1741
1760
|
}
|
|
1742
1761
|
while (true) {
|
|
1743
1762
|
if (callerID)
|
|
1744
|
-
await this
|
|
1763
|
+
await this.checkIfCanceledLimited(callerID);
|
|
1745
1764
|
let rows;
|
|
1746
1765
|
try {
|
|
1747
1766
|
({ rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT status, output, error, serialization FROM "${this.schemaName}".workflow_status
|
|
@@ -1786,7 +1805,7 @@ class SystemDatabase {
|
|
|
1786
1805
|
const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalResultMs;
|
|
1787
1806
|
while (true) {
|
|
1788
1807
|
if (callerID)
|
|
1789
|
-
await this
|
|
1808
|
+
await this.checkIfCanceledLimited(callerID);
|
|
1790
1809
|
const { rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status
|
|
1791
1810
|
WHERE workflow_uuid IN (${placeholders})
|
|
1792
1811
|
AND status NOT IN ('${workflow_1.StatusString.PENDING}', '${workflow_1.StatusString.ENQUEUED}', '${workflow_1.StatusString.DELAYED}')
|
|
@@ -1803,7 +1822,7 @@ class SystemDatabase {
|
|
|
1803
1822
|
while (remainingWorkflowIds.size > 0) {
|
|
1804
1823
|
const currentWorkflowIds = [...remainingWorkflowIds];
|
|
1805
1824
|
if (callerID)
|
|
1806
|
-
await this
|
|
1825
|
+
await this.checkIfCanceledLimited(callerID);
|
|
1807
1826
|
const { rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status
|
|
1808
1827
|
WHERE workflow_uuid = ANY($1::text[])
|
|
1809
1828
|
AND status NOT IN ('${workflow_1.StatusString.PENDING}', '${workflow_1.StatusString.ENQUEUED}', '${workflow_1.StatusString.DELAYED}')`, [currentWorkflowIds]));
|
|
@@ -1910,7 +1929,7 @@ class SystemDatabase {
|
|
|
1910
1929
|
const payload = `${workflowID}::${topic}`;
|
|
1911
1930
|
const cbr = this.notificationsMap.registerCallback(payload, resolveNotification);
|
|
1912
1931
|
try {
|
|
1913
|
-
await this
|
|
1932
|
+
await this.checkIfCanceledLimited(workflowID);
|
|
1914
1933
|
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
|
1915
1934
|
const initRecvRows = (await this.#pollWithLimiter(() => this.pool.query(`SELECT topic FROM "${this.schemaName}".notifications WHERE destination_uuid=$1 AND topic=$2 AND consumed = false;`, [workflowID, topic]))).rows;
|
|
1916
1935
|
if (initRecvRows.length !== 0)
|
|
@@ -2045,7 +2064,7 @@ class SystemDatabase {
|
|
|
2045
2064
|
const cbr = this.workflowEventsMap.registerCallback(payloadKey, resolveNotification);
|
|
2046
2065
|
try {
|
|
2047
2066
|
if (callerWorkflow?.workflowID)
|
|
2048
|
-
await this
|
|
2067
|
+
await this.checkIfCanceledLimited(callerWorkflow?.workflowID);
|
|
2049
2068
|
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
|
2050
2069
|
const initRecvRows = (await this.#pollWithLimiter(() => this.pool.query(`SELECT key, value, serialization
|
|
2051
2070
|
FROM "${this.schemaName}".workflow_events
|
|
@@ -2124,67 +2143,79 @@ class SystemDatabase {
|
|
|
2124
2143
|
}
|
|
2125
2144
|
// ==================== Streams ====================
|
|
2126
2145
|
async writeStreamFromStep(workflowID, functionID, key, serializedValue, serialization) {
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2146
|
+
while (true) {
|
|
2147
|
+
try {
|
|
2148
|
+
// Derives the first unused offset inside the insert; two writers can still pick the same one.
|
|
2149
|
+
await this.pool.query(`INSERT INTO "${this.schemaName}".streams (workflow_uuid, key, value, "offset", function_id, serialization)
|
|
2150
|
+
SELECT $1::text, $2::text, $3::text, COALESCE(MAX(s."offset"), -1) + 1, $4::int, $5::text
|
|
2151
|
+
FROM "${this.schemaName}".streams s
|
|
2152
|
+
WHERE s.workflow_uuid = $1 AND s.key = $2`, [workflowID, key, serializedValue, functionID, serialization]);
|
|
2153
|
+
}
|
|
2154
|
+
catch (e) {
|
|
2155
|
+
// Only an offset conflict resolves on retry; anything else would spin forever.
|
|
2156
|
+
if (e instanceof pg_1.DatabaseError && e.code === '23505') {
|
|
2157
|
+
this.logger.warn(`Stream offset conflict for workflow ${workflowID}, key ${key}; retrying`);
|
|
2158
|
+
await (0, utils_1.sleepms)(100);
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
this.logger.error(e);
|
|
2162
|
+
throw e;
|
|
2163
|
+
}
|
|
2140
2164
|
// Notify only after commit, so a woken reader sees the value.
|
|
2141
2165
|
this.#signalNotification(exports.DBOS_STREAMS_CHANNEL, `${workflowID}::${key}`);
|
|
2142
|
-
|
|
2143
|
-
catch (e) {
|
|
2144
|
-
this.logger.error(e);
|
|
2145
|
-
await client.query('ROLLBACK');
|
|
2146
|
-
throw e;
|
|
2147
|
-
}
|
|
2148
|
-
finally {
|
|
2149
|
-
client.release();
|
|
2166
|
+
return;
|
|
2150
2167
|
}
|
|
2151
2168
|
}
|
|
2152
2169
|
async writeStreamFromWorkflow(workflowID, functionID, key, serializedValue, serialization, functionName) {
|
|
2153
2170
|
const client = await this.#connect();
|
|
2154
2171
|
try {
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2172
|
+
while (true) {
|
|
2173
|
+
// Only a real insert (not a replay) should wake readers.
|
|
2174
|
+
let didWrite = false;
|
|
2175
|
+
try {
|
|
2176
|
+
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
2177
|
+
await this.#runAndRecordResult(client, functionName, workflowID, functionID, async () => {
|
|
2178
|
+
// Derives the first unused offset inside the insert; two writers can still pick the same one.
|
|
2179
|
+
await client.query(`INSERT INTO "${this.schemaName}".streams (workflow_uuid, key, value, "offset", function_id, serialization)
|
|
2180
|
+
SELECT $1::text, $2::text, $3::text, COALESCE(MAX(s."offset"), -1) + 1, $4::int, $5::text
|
|
2181
|
+
FROM "${this.schemaName}".streams s
|
|
2182
|
+
WHERE s.workflow_uuid = $1 AND s.key = $2`, [workflowID, key, serializedValue, functionID, serialization]);
|
|
2183
|
+
didWrite = true;
|
|
2184
|
+
return undefined;
|
|
2185
|
+
});
|
|
2186
|
+
await client.query('COMMIT');
|
|
2187
|
+
}
|
|
2188
|
+
catch (e) {
|
|
2189
|
+
// Only an offset conflict resolves on retry; anything else would spin forever.
|
|
2190
|
+
const offsetConflict = e instanceof pg_1.DatabaseError && e.code === '23505';
|
|
2191
|
+
// Log before touching the connection again: a failing ROLLBACK is what would propagate.
|
|
2192
|
+
if (!offsetConflict)
|
|
2193
|
+
this.logger.error(e);
|
|
2194
|
+
// Roll back before waiting, so a retry does not hold an aborted transaction open.
|
|
2195
|
+
await client.query('ROLLBACK');
|
|
2196
|
+
if (offsetConflict) {
|
|
2197
|
+
this.logger.warn(`Stream offset conflict for workflow ${workflowID}, key ${key}; retrying`);
|
|
2198
|
+
await (0, utils_1.sleepms)(100);
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
throw e;
|
|
2202
|
+
}
|
|
2203
|
+
// Notify only after commit, so a woken reader sees the value.
|
|
2204
|
+
if (didWrite) {
|
|
2205
|
+
this.#signalNotification(exports.DBOS_STREAMS_CHANNEL, `${workflowID}::${key}`);
|
|
2206
|
+
}
|
|
2207
|
+
return;
|
|
2175
2208
|
}
|
|
2176
2209
|
}
|
|
2177
|
-
catch (e) {
|
|
2178
|
-
this.logger.error(e);
|
|
2179
|
-
await client.query('ROLLBACK');
|
|
2180
|
-
throw e;
|
|
2181
|
-
}
|
|
2182
2210
|
finally {
|
|
2183
2211
|
client.release();
|
|
2184
2212
|
}
|
|
2185
2213
|
}
|
|
2186
|
-
async
|
|
2187
|
-
await this.writeStreamFromWorkflow(workflowID, functionID, key, exports.
|
|
2214
|
+
async closeStreamFromWorkflow(workflowID, functionID, key) {
|
|
2215
|
+
await this.writeStreamFromWorkflow(workflowID, functionID, key, exports.DBOS_STREAM_CLOSED_SENTINEL_SERIALIZED, serialization_1.DBOSPortableJSON.name(), exports.DBOS_FUNCNAME_CLOSESTREAM);
|
|
2216
|
+
}
|
|
2217
|
+
async closeStreamFromStep(workflowID, stepID, key) {
|
|
2218
|
+
await this.writeStreamFromStep(workflowID, stepID, key, exports.DBOS_STREAM_CLOSED_SENTINEL_SERIALIZED, serialization_1.DBOSPortableJSON.name());
|
|
2188
2219
|
}
|
|
2189
2220
|
// Read the value at `offset` and the workflow's status in one query: status null = no such workflow, value undefined = nothing at that offset.
|
|
2190
2221
|
async readStreamValue(workflowID, key, offset) {
|
|
@@ -2325,15 +2356,20 @@ class SystemDatabase {
|
|
|
2325
2356
|
WHERE workflow_uuid = $1
|
|
2326
2357
|
ORDER BY key, "offset"`, [workflowID]);
|
|
2327
2358
|
const streams = {};
|
|
2359
|
+
const closed = new Set();
|
|
2328
2360
|
for (const row of result.rows) {
|
|
2329
|
-
|
|
2330
|
-
if (value === exports.DBOS_STREAM_CLOSED_SENTINEL) {
|
|
2361
|
+
if (closed.has(row.key)) {
|
|
2331
2362
|
continue;
|
|
2332
2363
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2364
|
+
// safeParse yields the raw string for the legacy unserialized marker, which does not parse.
|
|
2365
|
+
const value = await (0, serialization_1.safeParse)(this.serializer, row.value, row.serialization);
|
|
2366
|
+
if (isStreamClosedSentinel(value)) {
|
|
2367
|
+
// End the stream where readStream does, so the two never disagree.
|
|
2368
|
+
closed.add(row.key);
|
|
2369
|
+
streams[row.key] ??= [];
|
|
2370
|
+
continue;
|
|
2335
2371
|
}
|
|
2336
|
-
streams[row.key].push(value);
|
|
2372
|
+
(streams[row.key] ??= []).push(value);
|
|
2337
2373
|
}
|
|
2338
2374
|
return streams;
|
|
2339
2375
|
}
|