@xmemory/temporal 1.0.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/LICENSE +21 -0
- package/NOTICE +27 -0
- package/README.md +371 -0
- package/dist/activities.d.ts +27 -0
- package/dist/activities.js +195 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +114 -0
- package/dist/deadline.d.ts +32 -0
- package/dist/deadline.js +50 -0
- package/dist/defaults.d.ts +49 -0
- package/dist/defaults.js +58 -0
- package/dist/dto.d.ts +54 -0
- package/dist/dto.js +83 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +247 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +38 -0
- package/dist/interceptor.d.ts +38 -0
- package/dist/interceptor.js +113 -0
- package/dist/names.d.ts +29 -0
- package/dist/names.js +47 -0
- package/dist/plugin.d.ts +31 -0
- package/dist/plugin.js +81 -0
- package/dist/workflow.d.ts +130 -0
- package/dist/workflow.js +412 -0
- package/package.json +70 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Translate xmemory API errors into Temporal failures.
|
|
4
|
+
*
|
|
5
|
+
* Temporal owns retries, so this is the one place an `XmemoryAPIError` becomes an
|
|
6
|
+
* `ApplicationFailure` with a retryability verdict.
|
|
7
|
+
*
|
|
8
|
+
* Rules: branch on `.code`, not the HTTP status; an unrecognized code never raises
|
|
9
|
+
* and keeps its own type, so a newer server cannot break this client mid-deploy,
|
|
10
|
+
* while whether to retry it comes from the status — a 401 or 404 is terminal
|
|
11
|
+
* whatever the code says; never echo the raw exception string, which can embed
|
|
12
|
+
* internal hostnames.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.NON_RETRYABLE_TYPES = exports.TYPE_WRITE_TIMEOUT = exports.TYPE_WRITE_NOT_FOUND = exports.TYPE_WRITE_FAILED = exports.TYPE_UNKNOWN = exports.TYPE_UNAVAILABLE = exports.TYPE_SERVER_ERROR = exports.TYPE_SCHEMA_REJECTED = exports.TYPE_RATE_LIMITED = exports.TYPE_QUOTA_EXCEEDED = exports.TYPE_NO_DEADLINE = exports.TYPE_NOT_FOUND = exports.TYPE_NOT_BOUND = exports.TYPE_MONTHLY_QUOTA_EXCEEDED = exports.TYPE_DEADLINE_EXPIRED = exports.TYPE_DAILY_QUOTA_EXCEEDED = exports.TYPE_BAD_REQUEST = exports.TYPE_BAD_OPTIONS = exports.TYPE_AUTH_FAILED = void 0;
|
|
16
|
+
exports.toApplicationFailure = toApplicationFailure;
|
|
17
|
+
const common_1 = require("@temporalio/common");
|
|
18
|
+
const xmemory_1 = require("xmemory");
|
|
19
|
+
const names_1 = require("./names");
|
|
20
|
+
Object.defineProperty(exports, "TYPE_AUTH_FAILED", { enumerable: true, get: function () { return names_1.TYPE_AUTH_FAILED; } });
|
|
21
|
+
Object.defineProperty(exports, "TYPE_BAD_OPTIONS", { enumerable: true, get: function () { return names_1.TYPE_BAD_OPTIONS; } });
|
|
22
|
+
Object.defineProperty(exports, "TYPE_BAD_REQUEST", { enumerable: true, get: function () { return names_1.TYPE_BAD_REQUEST; } });
|
|
23
|
+
Object.defineProperty(exports, "TYPE_DAILY_QUOTA_EXCEEDED", { enumerable: true, get: function () { return names_1.TYPE_DAILY_QUOTA_EXCEEDED; } });
|
|
24
|
+
Object.defineProperty(exports, "TYPE_DEADLINE_EXPIRED", { enumerable: true, get: function () { return names_1.TYPE_DEADLINE_EXPIRED; } });
|
|
25
|
+
Object.defineProperty(exports, "TYPE_MONTHLY_QUOTA_EXCEEDED", { enumerable: true, get: function () { return names_1.TYPE_MONTHLY_QUOTA_EXCEEDED; } });
|
|
26
|
+
Object.defineProperty(exports, "TYPE_NOT_BOUND", { enumerable: true, get: function () { return names_1.TYPE_NOT_BOUND; } });
|
|
27
|
+
Object.defineProperty(exports, "TYPE_NOT_FOUND", { enumerable: true, get: function () { return names_1.TYPE_NOT_FOUND; } });
|
|
28
|
+
Object.defineProperty(exports, "TYPE_NO_DEADLINE", { enumerable: true, get: function () { return names_1.TYPE_NO_DEADLINE; } });
|
|
29
|
+
Object.defineProperty(exports, "TYPE_QUOTA_EXCEEDED", { enumerable: true, get: function () { return names_1.TYPE_QUOTA_EXCEEDED; } });
|
|
30
|
+
Object.defineProperty(exports, "TYPE_RATE_LIMITED", { enumerable: true, get: function () { return names_1.TYPE_RATE_LIMITED; } });
|
|
31
|
+
Object.defineProperty(exports, "TYPE_SCHEMA_REJECTED", { enumerable: true, get: function () { return names_1.TYPE_SCHEMA_REJECTED; } });
|
|
32
|
+
Object.defineProperty(exports, "TYPE_SERVER_ERROR", { enumerable: true, get: function () { return names_1.TYPE_SERVER_ERROR; } });
|
|
33
|
+
Object.defineProperty(exports, "TYPE_UNAVAILABLE", { enumerable: true, get: function () { return names_1.TYPE_UNAVAILABLE; } });
|
|
34
|
+
Object.defineProperty(exports, "TYPE_UNKNOWN", { enumerable: true, get: function () { return names_1.TYPE_UNKNOWN; } });
|
|
35
|
+
Object.defineProperty(exports, "TYPE_WRITE_FAILED", { enumerable: true, get: function () { return names_1.TYPE_WRITE_FAILED; } });
|
|
36
|
+
Object.defineProperty(exports, "TYPE_WRITE_NOT_FOUND", { enumerable: true, get: function () { return names_1.TYPE_WRITE_NOT_FOUND; } });
|
|
37
|
+
Object.defineProperty(exports, "TYPE_WRITE_TIMEOUT", { enumerable: true, get: function () { return names_1.TYPE_WRITE_TIMEOUT; } });
|
|
38
|
+
exports.NON_RETRYABLE_TYPES = [
|
|
39
|
+
names_1.TYPE_MONTHLY_QUOTA_EXCEEDED,
|
|
40
|
+
names_1.TYPE_QUOTA_EXCEEDED,
|
|
41
|
+
names_1.TYPE_AUTH_FAILED,
|
|
42
|
+
names_1.TYPE_NOT_FOUND,
|
|
43
|
+
names_1.TYPE_BAD_REQUEST,
|
|
44
|
+
names_1.TYPE_SCHEMA_REJECTED,
|
|
45
|
+
names_1.TYPE_WRITE_FAILED,
|
|
46
|
+
names_1.TYPE_WRITE_NOT_FOUND,
|
|
47
|
+
names_1.TYPE_WRITE_TIMEOUT,
|
|
48
|
+
names_1.TYPE_BAD_OPTIONS,
|
|
49
|
+
names_1.TYPE_NOT_BOUND,
|
|
50
|
+
names_1.TYPE_NO_DEADLINE,
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* A server-supplied code, if this package recognises it.
|
|
54
|
+
*
|
|
55
|
+
* The code outlives the request in the Worker log and in failure `details`, which
|
|
56
|
+
* Temporal persists in the clear — and a shape test is no defence, since a leaked
|
|
57
|
+
* key looks exactly like an identifier. So only codes this module branches on are
|
|
58
|
+
* echoed; the rest become a fixed marker, not a digest (an unsalted hash of a
|
|
59
|
+
* low-entropy value is recoverable, and leaks equality). Matching uses the raw value.
|
|
60
|
+
*/
|
|
61
|
+
const UNRECOGNIZED = '<unrecognized>';
|
|
62
|
+
function safeCode(code) {
|
|
63
|
+
if (typeof code !== 'string')
|
|
64
|
+
return null;
|
|
65
|
+
return KNOWN_CODES.has(code) ? code : UNRECOGNIZED;
|
|
66
|
+
}
|
|
67
|
+
// Fixed, history-safe messages. Never include the raw exception string.
|
|
68
|
+
const MESSAGES = {
|
|
69
|
+
[names_1.TYPE_UNAVAILABLE]: 'xmemory is unreachable',
|
|
70
|
+
[names_1.TYPE_SERVER_ERROR]: 'xmemory returned a server error',
|
|
71
|
+
[names_1.TYPE_RATE_LIMITED]: 'xmemory rate-limited the request',
|
|
72
|
+
[names_1.TYPE_DAILY_QUOTA_EXCEEDED]: 'xmemory daily quota exceeded',
|
|
73
|
+
[names_1.TYPE_MONTHLY_QUOTA_EXCEEDED]: 'xmemory monthly quota exceeded',
|
|
74
|
+
[names_1.TYPE_QUOTA_EXCEEDED]: 'xmemory quota exceeded',
|
|
75
|
+
[names_1.TYPE_AUTH_FAILED]: 'xmemory rejected the credentials',
|
|
76
|
+
[names_1.TYPE_NOT_FOUND]: 'xmemory resource not found',
|
|
77
|
+
[names_1.TYPE_BAD_REQUEST]: 'xmemory rejected the request as invalid',
|
|
78
|
+
[names_1.TYPE_SCHEMA_REJECTED]: 'xmemory rejected the schema change',
|
|
79
|
+
[names_1.TYPE_UNKNOWN]: 'xmemory returned an unrecognized error',
|
|
80
|
+
};
|
|
81
|
+
// 9999-12-31T23:59:59Z, where protobuf's Timestamp ends: a retry scheduled past it
|
|
82
|
+
// cannot be represented, and the server refuses the failure that carries it.
|
|
83
|
+
const MAX_TIMESTAMP_MS = 253_402_300_799_000;
|
|
84
|
+
const RETRYABLE_CODES = new Set(['INTERNAL_ERROR', 'SERVICE_UNAVAILABLE']);
|
|
85
|
+
const AUTH_CODES = new Set(['UNAUTHORIZED', 'FORBIDDEN']);
|
|
86
|
+
const BAD_REQUEST_CODES = new Set(['VALIDATION_ERROR', 'INVALID_INPUT', 'ALREADY_EXISTS', 'CONFLICT']);
|
|
87
|
+
// A queued write that exhausted its own retry budget; retrying cannot help.
|
|
88
|
+
const EXHAUSTED_CODES = new Set(['MAX_RETRIES_EXCEEDED']);
|
|
89
|
+
const SCHEMA_CODES = new Set([
|
|
90
|
+
'stale_proposal_version',
|
|
91
|
+
'stale_schema_version',
|
|
92
|
+
'dependency_closure_failed',
|
|
93
|
+
'destructive_confirmation_required',
|
|
94
|
+
'non_additive_change_requires_plan',
|
|
95
|
+
'migration_not_found',
|
|
96
|
+
'instance_not_initialised',
|
|
97
|
+
]);
|
|
98
|
+
/**
|
|
99
|
+
* The server's pacing hint in milliseconds, or `undefined` when Temporal cannot
|
|
100
|
+
* carry it. Never clamped — the Activity's own policy bounds the retrying.
|
|
101
|
+
*
|
|
102
|
+
* Sent as a number, not a `<n>s` string: the string form goes through a duration
|
|
103
|
+
* parser that rejects exponent notation. Dropped only when unusable — `Infinity`,
|
|
104
|
+
* past `MAX_SAFE_INTEGER` milliseconds where the int64 conversion saturates, or a
|
|
105
|
+
* delay landing past protobuf's 9999-12-31 Timestamp ceiling, which the server
|
|
106
|
+
* rejects when the failure is reported.
|
|
107
|
+
*/
|
|
108
|
+
// Every code this module branches on. Nothing else is ever echoed.
|
|
109
|
+
const KNOWN_CODES = new Set([
|
|
110
|
+
...RETRYABLE_CODES,
|
|
111
|
+
...AUTH_CODES,
|
|
112
|
+
...BAD_REQUEST_CODES,
|
|
113
|
+
...EXHAUSTED_CODES,
|
|
114
|
+
...SCHEMA_CODES,
|
|
115
|
+
'QUOTA_EXCEEDED',
|
|
116
|
+
'RATE_LIMITED',
|
|
117
|
+
'NOT_FOUND',
|
|
118
|
+
]);
|
|
119
|
+
function retryDelayMs(err) {
|
|
120
|
+
const detail = (err.details ?? {});
|
|
121
|
+
// Each source judged on its own: preferring the header and validating afterwards
|
|
122
|
+
// discarded a good structured hint whenever the header was unusable.
|
|
123
|
+
for (const hint of [err.retryAfter, detail.retry_after_seconds]) {
|
|
124
|
+
const ms = usableDelayMs(hint);
|
|
125
|
+
if (ms !== undefined)
|
|
126
|
+
return ms;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
/** A hint in milliseconds, or `undefined` when Temporal could not carry it. */
|
|
131
|
+
function usableDelayMs(hint) {
|
|
132
|
+
if (typeof hint !== 'number' || hint <= 0)
|
|
133
|
+
return undefined;
|
|
134
|
+
const ms = Math.round(hint * 1000);
|
|
135
|
+
if (!Number.isSafeInteger(ms) || ms < 1)
|
|
136
|
+
return undefined;
|
|
137
|
+
if (ms > MAX_TIMESTAMP_MS - Date.now())
|
|
138
|
+
return undefined;
|
|
139
|
+
return ms;
|
|
140
|
+
}
|
|
141
|
+
function quotaVerdict(err) {
|
|
142
|
+
const detail = (err.details ?? {});
|
|
143
|
+
const kind = detail.kind;
|
|
144
|
+
// A daily window resets within hours and is worth retrying; a monthly one is not.
|
|
145
|
+
// An unknown kind falls back to non-retryable.
|
|
146
|
+
if (kind === 'daily_quota_exceeded')
|
|
147
|
+
return { type: names_1.TYPE_DAILY_QUOTA_EXCEEDED, retryable: true };
|
|
148
|
+
if (kind === 'monthly_quota_exceeded')
|
|
149
|
+
return { type: names_1.TYPE_MONTHLY_QUOTA_EXCEEDED, retryable: false };
|
|
150
|
+
return { type: names_1.TYPE_QUOTA_EXCEEDED, retryable: false };
|
|
151
|
+
}
|
|
152
|
+
function verdictFromStatus(status) {
|
|
153
|
+
if (status === undefined)
|
|
154
|
+
return { type: names_1.TYPE_UNAVAILABLE, retryable: true };
|
|
155
|
+
if (status === 408 || status >= 500)
|
|
156
|
+
return { type: names_1.TYPE_SERVER_ERROR, retryable: true };
|
|
157
|
+
if (status === 429)
|
|
158
|
+
return { type: names_1.TYPE_RATE_LIMITED, retryable: true };
|
|
159
|
+
if (status === 401 || status === 403)
|
|
160
|
+
return { type: names_1.TYPE_AUTH_FAILED, retryable: false };
|
|
161
|
+
if (status === 404)
|
|
162
|
+
return { type: names_1.TYPE_NOT_FOUND, retryable: false };
|
|
163
|
+
if (status >= 400)
|
|
164
|
+
return { type: names_1.TYPE_BAD_REQUEST, retryable: false };
|
|
165
|
+
return { type: names_1.TYPE_UNKNOWN, retryable: true };
|
|
166
|
+
}
|
|
167
|
+
function build(type, retryable, code = null, status = null, delayMs) {
|
|
168
|
+
return common_1.ApplicationFailure.create({
|
|
169
|
+
message: MESSAGES[type] ?? 'xmemory request failed',
|
|
170
|
+
type,
|
|
171
|
+
nonRetryable: !retryable,
|
|
172
|
+
// `retryAfterSeconds` rides in details as well as in nextRetryDelay: the SDK
|
|
173
|
+
// encodes the latter for the server's retry scheduling but drops it when
|
|
174
|
+
// decoding the failure back into workflow code, where the durable-write loop
|
|
175
|
+
// needs it to pace its own polling. Details survive that round trip.
|
|
176
|
+
details: [{ code, status, ...(delayMs !== undefined ? { retryAfterSeconds: delayMs / 1000 } : {}) }],
|
|
177
|
+
nextRetryDelay: retryable && delayMs !== undefined ? delayMs : undefined,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Whether a non-API error is a `fetch` network failure rather than a bug.
|
|
182
|
+
*
|
|
183
|
+
* Undici raises `TypeError('fetch failed', { cause })` for DNS failures,
|
|
184
|
+
* refused connections, and resets; a programming TypeError carries no `cause`.
|
|
185
|
+
* An `AbortError` from the client's own timeout is a `DOMException`, so it does
|
|
186
|
+
* not reach here.
|
|
187
|
+
*/
|
|
188
|
+
function isNetworkError(err) {
|
|
189
|
+
return err instanceof TypeError && Object.hasOwn(err, 'cause');
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Whether this is the client's own API error. `instanceof` alone is not enough:
|
|
193
|
+
* xmemory ships CJS and ESM builds, so a handle from the other half throws a
|
|
194
|
+
* different class object and an auth failure looked like a transport blip.
|
|
195
|
+
*/
|
|
196
|
+
function isApiError(err) {
|
|
197
|
+
return err instanceof xmemory_1.XmemoryAPIError || (err instanceof Error && err.name === 'XmemoryAPIError');
|
|
198
|
+
}
|
|
199
|
+
/** Map any client-raised error onto a Temporal `ApplicationFailure`. */
|
|
200
|
+
function toApplicationFailure(err) {
|
|
201
|
+
if (!isApiError(err)) {
|
|
202
|
+
// Not an API error: a transport failure (retryable) or a deterministic
|
|
203
|
+
// client-side error (non-retryable — a retry replays the same input). The
|
|
204
|
+
// raw message is never echoed (it may embed internal transport details).
|
|
205
|
+
//
|
|
206
|
+
// `fetch` rejects a network failure (DNS, refused, reset) with a TypeError,
|
|
207
|
+
// and the client does not wrap it, so a bare `instanceof TypeError` would
|
|
208
|
+
// make a transient blip permanently fatal. Undici sets `cause` on those and
|
|
209
|
+
// never on a programming TypeError, which is the discriminator.
|
|
210
|
+
if (isNetworkError(err))
|
|
211
|
+
return build(names_1.TYPE_UNAVAILABLE, true);
|
|
212
|
+
if (err instanceof TypeError || err instanceof RangeError || err instanceof SyntaxError) {
|
|
213
|
+
return build(names_1.TYPE_BAD_REQUEST, false);
|
|
214
|
+
}
|
|
215
|
+
return build(names_1.TYPE_UNAVAILABLE, true);
|
|
216
|
+
}
|
|
217
|
+
const { code, status } = err;
|
|
218
|
+
let verdict;
|
|
219
|
+
if (code === 'QUOTA_EXCEEDED')
|
|
220
|
+
verdict = quotaVerdict(err);
|
|
221
|
+
else if (code === 'RATE_LIMITED')
|
|
222
|
+
verdict = { type: names_1.TYPE_RATE_LIMITED, retryable: true };
|
|
223
|
+
else if (code && RETRYABLE_CODES.has(code))
|
|
224
|
+
verdict = { type: names_1.TYPE_SERVER_ERROR, retryable: true };
|
|
225
|
+
else if (code && AUTH_CODES.has(code))
|
|
226
|
+
verdict = { type: names_1.TYPE_AUTH_FAILED, retryable: false };
|
|
227
|
+
else if (code === 'NOT_FOUND')
|
|
228
|
+
verdict = { type: names_1.TYPE_NOT_FOUND, retryable: false };
|
|
229
|
+
else if (code && BAD_REQUEST_CODES.has(code))
|
|
230
|
+
verdict = { type: names_1.TYPE_BAD_REQUEST, retryable: false };
|
|
231
|
+
else if (code && EXHAUSTED_CODES.has(code))
|
|
232
|
+
verdict = { type: names_1.TYPE_WRITE_FAILED, retryable: false };
|
|
233
|
+
else if (code && SCHEMA_CODES.has(code))
|
|
234
|
+
verdict = { type: names_1.TYPE_SCHEMA_REJECTED, retryable: false };
|
|
235
|
+
else if (code) {
|
|
236
|
+
// Almost certainly a newer server: keep the type so the gap is findable, but
|
|
237
|
+
// take retryability from the status — a 401 is terminal whatever the code says.
|
|
238
|
+
// No status at all still gets the benefit of the doubt.
|
|
239
|
+
console.warn(`xmemory returned an unrecognized error code ${JSON.stringify(safeCode(code))} (HTTP ${status})`);
|
|
240
|
+
verdict = { type: names_1.TYPE_UNKNOWN, retryable: verdictFromStatus(status).retryable };
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
verdict = verdictFromStatus(status);
|
|
244
|
+
}
|
|
245
|
+
const delay = verdict.retryable ? retryDelayMs(err) : undefined;
|
|
246
|
+
return build(verdict.type, verdict.retryable, safeCode(code), status ?? null, delay);
|
|
247
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal plugin for xmemory — durable agent memory as Temporal Activities.
|
|
3
|
+
*
|
|
4
|
+
* Add {@link XmemoryPlugin} to your Temporal Worker. Workflow code imports its
|
|
5
|
+
* side of the API from `@xmemory/temporal/workflow`, a leaf that pulls in no
|
|
6
|
+
* Activity or client code, so Temporal's workflow bundler accepts it.
|
|
7
|
+
*/
|
|
8
|
+
export { XmemoryPlugin, PLUGIN_NAME } from './plugin';
|
|
9
|
+
export type { XmemoryPluginOptions } from './plugin';
|
|
10
|
+
export type { XmemoryConfig } from './config';
|
|
11
|
+
export { DEFAULT_TIMEOUTS, DEFAULT_CLIENT_MARGIN_MS } from './defaults';
|
|
12
|
+
export type { XmemoryTimeouts } from './defaults';
|
|
13
|
+
export type { WorkflowXmemory, WorkflowXmemoryOptions, WriteDurableOptions, WriteStatusRetry } from './workflow';
|
|
14
|
+
export type { AutoCaptureConfig } from './interceptor';
|
|
15
|
+
export { TYPE_AUTH_FAILED, TYPE_BAD_OPTIONS, TYPE_BAD_REQUEST, TYPE_DAILY_QUOTA_EXCEEDED, TYPE_DEADLINE_EXPIRED, TYPE_MONTHLY_QUOTA_EXCEEDED, TYPE_NOT_BOUND, TYPE_NOT_FOUND, TYPE_NO_DEADLINE, TYPE_QUOTA_EXCEEDED, TYPE_RATE_LIMITED, TYPE_SCHEMA_REJECTED, TYPE_SERVER_ERROR, TYPE_UNAVAILABLE, TYPE_UNKNOWN, TYPE_WRITE_FAILED, TYPE_WRITE_NOT_FOUND, TYPE_WRITE_TIMEOUT, } from './errors';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Temporal plugin for xmemory — durable agent memory as Temporal Activities.
|
|
4
|
+
*
|
|
5
|
+
* Add {@link XmemoryPlugin} to your Temporal Worker. Workflow code imports its
|
|
6
|
+
* side of the API from `@xmemory/temporal/workflow`, a leaf that pulls in no
|
|
7
|
+
* Activity or client code, so Temporal's workflow bundler accepts it.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.TYPE_WRITE_TIMEOUT = exports.TYPE_WRITE_NOT_FOUND = exports.TYPE_WRITE_FAILED = exports.TYPE_UNKNOWN = exports.TYPE_UNAVAILABLE = exports.TYPE_SERVER_ERROR = exports.TYPE_SCHEMA_REJECTED = exports.TYPE_RATE_LIMITED = exports.TYPE_QUOTA_EXCEEDED = exports.TYPE_NO_DEADLINE = exports.TYPE_NOT_FOUND = exports.TYPE_NOT_BOUND = exports.TYPE_MONTHLY_QUOTA_EXCEEDED = exports.TYPE_DEADLINE_EXPIRED = exports.TYPE_DAILY_QUOTA_EXCEEDED = exports.TYPE_BAD_REQUEST = exports.TYPE_BAD_OPTIONS = exports.TYPE_AUTH_FAILED = exports.DEFAULT_CLIENT_MARGIN_MS = exports.DEFAULT_TIMEOUTS = exports.PLUGIN_NAME = exports.XmemoryPlugin = void 0;
|
|
11
|
+
var plugin_1 = require("./plugin");
|
|
12
|
+
Object.defineProperty(exports, "XmemoryPlugin", { enumerable: true, get: function () { return plugin_1.XmemoryPlugin; } });
|
|
13
|
+
Object.defineProperty(exports, "PLUGIN_NAME", { enumerable: true, get: function () { return plugin_1.PLUGIN_NAME; } });
|
|
14
|
+
var defaults_1 = require("./defaults");
|
|
15
|
+
Object.defineProperty(exports, "DEFAULT_TIMEOUTS", { enumerable: true, get: function () { return defaults_1.DEFAULT_TIMEOUTS; } });
|
|
16
|
+
Object.defineProperty(exports, "DEFAULT_CLIENT_MARGIN_MS", { enumerable: true, get: function () { return defaults_1.DEFAULT_CLIENT_MARGIN_MS; } });
|
|
17
|
+
// The `type` strings themselves, so a consumer can match on them in a
|
|
18
|
+
// RetryPolicy without hard-coding literals. The export map has no deep-import
|
|
19
|
+
// escape hatch, so anything a caller needs has to leave through here.
|
|
20
|
+
var errors_1 = require("./errors");
|
|
21
|
+
Object.defineProperty(exports, "TYPE_AUTH_FAILED", { enumerable: true, get: function () { return errors_1.TYPE_AUTH_FAILED; } });
|
|
22
|
+
Object.defineProperty(exports, "TYPE_BAD_OPTIONS", { enumerable: true, get: function () { return errors_1.TYPE_BAD_OPTIONS; } });
|
|
23
|
+
Object.defineProperty(exports, "TYPE_BAD_REQUEST", { enumerable: true, get: function () { return errors_1.TYPE_BAD_REQUEST; } });
|
|
24
|
+
Object.defineProperty(exports, "TYPE_DAILY_QUOTA_EXCEEDED", { enumerable: true, get: function () { return errors_1.TYPE_DAILY_QUOTA_EXCEEDED; } });
|
|
25
|
+
Object.defineProperty(exports, "TYPE_DEADLINE_EXPIRED", { enumerable: true, get: function () { return errors_1.TYPE_DEADLINE_EXPIRED; } });
|
|
26
|
+
Object.defineProperty(exports, "TYPE_MONTHLY_QUOTA_EXCEEDED", { enumerable: true, get: function () { return errors_1.TYPE_MONTHLY_QUOTA_EXCEEDED; } });
|
|
27
|
+
Object.defineProperty(exports, "TYPE_NOT_BOUND", { enumerable: true, get: function () { return errors_1.TYPE_NOT_BOUND; } });
|
|
28
|
+
Object.defineProperty(exports, "TYPE_NOT_FOUND", { enumerable: true, get: function () { return errors_1.TYPE_NOT_FOUND; } });
|
|
29
|
+
Object.defineProperty(exports, "TYPE_NO_DEADLINE", { enumerable: true, get: function () { return errors_1.TYPE_NO_DEADLINE; } });
|
|
30
|
+
Object.defineProperty(exports, "TYPE_QUOTA_EXCEEDED", { enumerable: true, get: function () { return errors_1.TYPE_QUOTA_EXCEEDED; } });
|
|
31
|
+
Object.defineProperty(exports, "TYPE_RATE_LIMITED", { enumerable: true, get: function () { return errors_1.TYPE_RATE_LIMITED; } });
|
|
32
|
+
Object.defineProperty(exports, "TYPE_SCHEMA_REJECTED", { enumerable: true, get: function () { return errors_1.TYPE_SCHEMA_REJECTED; } });
|
|
33
|
+
Object.defineProperty(exports, "TYPE_SERVER_ERROR", { enumerable: true, get: function () { return errors_1.TYPE_SERVER_ERROR; } });
|
|
34
|
+
Object.defineProperty(exports, "TYPE_UNAVAILABLE", { enumerable: true, get: function () { return errors_1.TYPE_UNAVAILABLE; } });
|
|
35
|
+
Object.defineProperty(exports, "TYPE_UNKNOWN", { enumerable: true, get: function () { return errors_1.TYPE_UNKNOWN; } });
|
|
36
|
+
Object.defineProperty(exports, "TYPE_WRITE_FAILED", { enumerable: true, get: function () { return errors_1.TYPE_WRITE_FAILED; } });
|
|
37
|
+
Object.defineProperty(exports, "TYPE_WRITE_NOT_FOUND", { enumerable: true, get: function () { return errors_1.TYPE_WRITE_NOT_FOUND; } });
|
|
38
|
+
Object.defineProperty(exports, "TYPE_WRITE_TIMEOUT", { enumerable: true, get: function () { return errors_1.TYPE_WRITE_TIMEOUT; } });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in auto-capture of activity results into xmemory.
|
|
3
|
+
*
|
|
4
|
+
* An *activity* interceptor, not a workflow one: workflow interceptors re-run on
|
|
5
|
+
* every replay, so I/O there breaks determinism.
|
|
6
|
+
*
|
|
7
|
+
* Guardrails: `project` decides what to remember, `sampleRate` bounds fan-out,
|
|
8
|
+
* capture is clamped to what is left of the activity's deadline, and a capture
|
|
9
|
+
* failure never fails that activity.
|
|
10
|
+
*/
|
|
11
|
+
import type { ActivityInboundCallsInterceptor } from '@temporalio/worker';
|
|
12
|
+
import type { InstanceHolder } from './activities';
|
|
13
|
+
import type { XmemoryConfig } from './config';
|
|
14
|
+
export interface AutoCaptureConfig {
|
|
15
|
+
/**
|
|
16
|
+
* What to remember from a completed activity, or `undefined` to skip it.
|
|
17
|
+
*
|
|
18
|
+
* No default: raw payloads are JSON blobs the extraction engine cannot use.
|
|
19
|
+
*/
|
|
20
|
+
project: (activityName: string, result: unknown) => string | undefined | null;
|
|
21
|
+
/** Fraction of eligible activities to capture, in [0, 1]. Defaults to 1. */
|
|
22
|
+
sampleRate?: number;
|
|
23
|
+
extractionLogic?: 'fast' | 'deep';
|
|
24
|
+
/**
|
|
25
|
+
* Ceiling (ms) a capture may add to the wrapped activity. Lowered further when
|
|
26
|
+
* less than this is left of the activity's deadline.
|
|
27
|
+
*/
|
|
28
|
+
captureTimeoutMs?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Milliseconds capture may take, or `null` when it must be skipped.
|
|
32
|
+
*
|
|
33
|
+
* Capture spends the wrapped Activity's deadline, so an Activity that has nearly
|
|
34
|
+
* used its budget would be pushed past it and retried, discarding a result it had
|
|
35
|
+
* already produced.
|
|
36
|
+
*/
|
|
37
|
+
export declare function captureBudgetMs(remainingMs: number, ceilingMs: number, marginMs: number): number | null;
|
|
38
|
+
export declare function createAutoCaptureInterceptor(holder: InstanceHolder, config: XmemoryConfig, autoCapture: AutoCaptureConfig): ActivityInboundCallsInterceptor;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Opt-in auto-capture of activity results into xmemory.
|
|
4
|
+
*
|
|
5
|
+
* An *activity* interceptor, not a workflow one: workflow interceptors re-run on
|
|
6
|
+
* every replay, so I/O there breaks determinism.
|
|
7
|
+
*
|
|
8
|
+
* Guardrails: `project` decides what to remember, `sampleRate` bounds fan-out,
|
|
9
|
+
* capture is clamped to what is left of the activity's deadline, and a capture
|
|
10
|
+
* failure never fails that activity.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.captureBudgetMs = captureBudgetMs;
|
|
14
|
+
exports.createAutoCaptureInterceptor = createAutoCaptureInterceptor;
|
|
15
|
+
const activity_1 = require("@temporalio/activity");
|
|
16
|
+
const deadline_1 = require("./deadline");
|
|
17
|
+
const defaults_1 = require("./defaults");
|
|
18
|
+
// Never capture our own writes, or capture recurses. This also skips a user
|
|
19
|
+
// activity named `xmemory_*` — see the README's auto-capture section.
|
|
20
|
+
const OWN_ACTIVITY_PREFIX = 'xmemory_';
|
|
21
|
+
/**
|
|
22
|
+
* Milliseconds capture may take, or `null` when it must be skipped.
|
|
23
|
+
*
|
|
24
|
+
* Capture spends the wrapped Activity's deadline, so an Activity that has nearly
|
|
25
|
+
* used its budget would be pushed past it and retried, discarding a result it had
|
|
26
|
+
* already produced.
|
|
27
|
+
*/
|
|
28
|
+
function captureBudgetMs(remainingMs, ceilingMs, marginMs) {
|
|
29
|
+
// A margin of zero or less would leave no completion gap, or hand capture more
|
|
30
|
+
// time than the Activity has left. Fall back to a proportional reserve.
|
|
31
|
+
const usable = Number.isFinite(marginMs) && marginMs > 0 ? remainingMs - marginMs : remainingMs * 0.8;
|
|
32
|
+
if (usable <= 0 || !(ceilingMs > 0))
|
|
33
|
+
return null;
|
|
34
|
+
// Capped like every client budget: a Node timer past it fires after 1ms.
|
|
35
|
+
return Math.min(ceilingMs, usable, defaults_1.MAX_DURATION_MS);
|
|
36
|
+
}
|
|
37
|
+
function createAutoCaptureInterceptor(holder, config, autoCapture) {
|
|
38
|
+
const sampleRate = autoCapture.sampleRate ?? 1;
|
|
39
|
+
const extractionLogic = autoCapture.extractionLogic ?? 'fast';
|
|
40
|
+
const ceilingMs = autoCapture.captureTimeoutMs ?? 5000;
|
|
41
|
+
const marginMs = config.clientMarginMs ?? defaults_1.DEFAULT_CLIENT_MARGIN_MS;
|
|
42
|
+
return {
|
|
43
|
+
async execute(input, next) {
|
|
44
|
+
const started = performance.now();
|
|
45
|
+
const result = await next(input);
|
|
46
|
+
try {
|
|
47
|
+
await maybeCapture(result, started);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Capture must never fail the wrapped activity: errors and timeouts alike.
|
|
51
|
+
console.warn('xmemory auto-capture skipped; the wrapped activity is unaffected');
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
async function maybeCapture(result, started) {
|
|
57
|
+
const info = activity_1.Context.current().info;
|
|
58
|
+
const name = info.activityType;
|
|
59
|
+
if (name.startsWith(OWN_ACTIVITY_PREFIX))
|
|
60
|
+
return;
|
|
61
|
+
if (!shouldSample())
|
|
62
|
+
return;
|
|
63
|
+
// Budget before projecting: `project` is the caller's code and can itself be
|
|
64
|
+
// slow enough to push the Activity past its deadline.
|
|
65
|
+
const budgetMs = captureBudgetMs(remainingMs(info, started), ceilingMs, marginMs);
|
|
66
|
+
if (budgetMs === null)
|
|
67
|
+
return;
|
|
68
|
+
const text = autoCapture.project(name, result);
|
|
69
|
+
if (!text)
|
|
70
|
+
return;
|
|
71
|
+
if (typeof text !== 'string') {
|
|
72
|
+
// `writeAsync` is overloaded on its first argument, so an array would be
|
|
73
|
+
// applied as structured mutations rather than remembered. Skipped, like every
|
|
74
|
+
// capture problem: it never affects the wrapped Activity.
|
|
75
|
+
console.warn(`xmemory auto-capture skipped: project() returned a ${typeof text}, expected a string`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Re-checked, because the projector just spent some of the same budget.
|
|
79
|
+
const enqueueMs = captureBudgetMs(remainingMs(info, started), ceilingMs, marginMs);
|
|
80
|
+
if (enqueueMs === null)
|
|
81
|
+
return;
|
|
82
|
+
// Enqueue, not a full write: waiting for extraction would add its latency to
|
|
83
|
+
// the activity's budget. Two bounds because `timeoutMs` stops at headers and
|
|
84
|
+
// `withDeadline` caps the total.
|
|
85
|
+
await (0, deadline_1.withDeadline)(holder.get().writeAsync(text, { extractionLogic, timeoutMs: enqueueMs }), enqueueMs);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What is left of the wrapped Activity's deadline, or zero when it has none.
|
|
89
|
+
*
|
|
90
|
+
* `started` is stamped before the Activity runs, so the elapsed figure covers it
|
|
91
|
+
* and every interceptor inside this one. Handed to `activityBudgetMs` rather than
|
|
92
|
+
* subtracted here, since only the start-to-close bound needs it.
|
|
93
|
+
*/
|
|
94
|
+
function remainingMs(info, started) {
|
|
95
|
+
return (0, deadline_1.activityBudgetMs)(info, performance.now() - started) ?? 0;
|
|
96
|
+
}
|
|
97
|
+
function shouldSample() {
|
|
98
|
+
if (sampleRate >= 1)
|
|
99
|
+
return true;
|
|
100
|
+
if (sampleRate <= 0)
|
|
101
|
+
return false;
|
|
102
|
+
// Keyed on the run as well as the activity: `activityId` restarts at "1" in
|
|
103
|
+
// every workflow, so hashing it alone bucketed them all together.
|
|
104
|
+
const info = activity_1.Context.current().info;
|
|
105
|
+
const id = `${info.workflowExecution?.runId ?? ''}/${info.activityId}`;
|
|
106
|
+
let hash = 0;
|
|
107
|
+
for (let i = 0; i < id.length; i++)
|
|
108
|
+
hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
|
|
109
|
+
// A million buckets, so a rate below 1/1000 still works. Low bits: this
|
|
110
|
+
// accumulator mixes them well, its high bits barely move across similar ids.
|
|
111
|
+
return (hash % 1_000_000) / 1_000_000 < sampleRate;
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/names.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity name constants, in a leaf module with no client imports.
|
|
3
|
+
*
|
|
4
|
+
* The workflow bundle imports these; keeping them here (rather than in
|
|
5
|
+
* `activities.ts`, which imports the xmemory client) keeps the client out of
|
|
6
|
+
* the workflow sandbox bundle.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ACTIVITY_READ = "xmemory_read";
|
|
9
|
+
export declare const ACTIVITY_WRITE = "xmemory_write";
|
|
10
|
+
export declare const ACTIVITY_WRITE_START = "xmemory_write_start";
|
|
11
|
+
export declare const ACTIVITY_WRITE_STATUS = "xmemory_write_status";
|
|
12
|
+
export declare const TYPE_WRITE_FAILED = "XmemoryWriteFailed";
|
|
13
|
+
export declare const TYPE_WRITE_NOT_FOUND = "XmemoryWriteNotFound";
|
|
14
|
+
export declare const TYPE_WRITE_TIMEOUT = "XmemoryWriteTimeout";
|
|
15
|
+
export declare const TYPE_BAD_OPTIONS = "XmemoryBadOptions";
|
|
16
|
+
export declare const TYPE_UNAVAILABLE = "XmemoryUnavailable";
|
|
17
|
+
export declare const TYPE_SERVER_ERROR = "XmemoryServerError";
|
|
18
|
+
export declare const TYPE_RATE_LIMITED = "XmemoryRateLimited";
|
|
19
|
+
export declare const TYPE_DAILY_QUOTA_EXCEEDED = "XmemoryDailyQuotaExceeded";
|
|
20
|
+
export declare const TYPE_MONTHLY_QUOTA_EXCEEDED = "XmemoryMonthlyQuotaExceeded";
|
|
21
|
+
export declare const TYPE_QUOTA_EXCEEDED = "XmemoryQuotaExceeded";
|
|
22
|
+
export declare const TYPE_AUTH_FAILED = "XmemoryAuthFailed";
|
|
23
|
+
export declare const TYPE_NOT_FOUND = "XmemoryNotFound";
|
|
24
|
+
export declare const TYPE_BAD_REQUEST = "XmemoryBadRequest";
|
|
25
|
+
export declare const TYPE_SCHEMA_REJECTED = "XmemorySchemaRejected";
|
|
26
|
+
export declare const TYPE_NOT_BOUND = "XmemoryNotBound";
|
|
27
|
+
export declare const TYPE_NO_DEADLINE = "XmemoryNoDeadline";
|
|
28
|
+
export declare const TYPE_DEADLINE_EXPIRED = "XmemoryDeadlineExpired";
|
|
29
|
+
export declare const TYPE_UNKNOWN = "XmemoryUnknown";
|
package/dist/names.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Activity name constants, in a leaf module with no client imports.
|
|
4
|
+
*
|
|
5
|
+
* The workflow bundle imports these; keeping them here (rather than in
|
|
6
|
+
* `activities.ts`, which imports the xmemory client) keeps the client out of
|
|
7
|
+
* the workflow sandbox bundle.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.TYPE_UNKNOWN = exports.TYPE_DEADLINE_EXPIRED = exports.TYPE_NO_DEADLINE = exports.TYPE_NOT_BOUND = exports.TYPE_SCHEMA_REJECTED = exports.TYPE_BAD_REQUEST = exports.TYPE_NOT_FOUND = exports.TYPE_AUTH_FAILED = exports.TYPE_QUOTA_EXCEEDED = exports.TYPE_MONTHLY_QUOTA_EXCEEDED = exports.TYPE_DAILY_QUOTA_EXCEEDED = exports.TYPE_RATE_LIMITED = exports.TYPE_SERVER_ERROR = exports.TYPE_UNAVAILABLE = exports.TYPE_BAD_OPTIONS = exports.TYPE_WRITE_TIMEOUT = exports.TYPE_WRITE_NOT_FOUND = exports.TYPE_WRITE_FAILED = exports.ACTIVITY_WRITE_STATUS = exports.ACTIVITY_WRITE_START = exports.ACTIVITY_WRITE = exports.ACTIVITY_READ = void 0;
|
|
11
|
+
exports.ACTIVITY_READ = 'xmemory_read';
|
|
12
|
+
exports.ACTIVITY_WRITE = 'xmemory_write';
|
|
13
|
+
exports.ACTIVITY_WRITE_START = 'xmemory_write_start';
|
|
14
|
+
exports.ACTIVITY_WRITE_STATUS = 'xmemory_write_status';
|
|
15
|
+
// Job-level write outcomes raised by the durable poll loop (workflow.ts). A
|
|
16
|
+
// public `RetryPolicy.nonRetryableErrorTypes` contract; kept in this client-free
|
|
17
|
+
// leaf so the workflow bundle can import them without pulling the client in, and
|
|
18
|
+
// re-exported from errors.ts so the full type= set lives in one logical place.
|
|
19
|
+
exports.TYPE_WRITE_FAILED = 'XmemoryWriteFailed';
|
|
20
|
+
exports.TYPE_WRITE_NOT_FOUND = 'XmemoryWriteNotFound';
|
|
21
|
+
exports.TYPE_WRITE_TIMEOUT = 'XmemoryWriteTimeout';
|
|
22
|
+
// A caller's own durable-write options are unusable. Non-retryable: the same
|
|
23
|
+
// arguments would be rejected identically on every attempt.
|
|
24
|
+
exports.TYPE_BAD_OPTIONS = 'XmemoryBadOptions';
|
|
25
|
+
// Stable `type` strings — a public contract for
|
|
26
|
+
// `RetryPolicy.nonRetryableErrorTypes`. Renaming one is a breaking change.
|
|
27
|
+
exports.TYPE_UNAVAILABLE = 'XmemoryUnavailable';
|
|
28
|
+
exports.TYPE_SERVER_ERROR = 'XmemoryServerError';
|
|
29
|
+
exports.TYPE_RATE_LIMITED = 'XmemoryRateLimited';
|
|
30
|
+
exports.TYPE_DAILY_QUOTA_EXCEEDED = 'XmemoryDailyQuotaExceeded';
|
|
31
|
+
exports.TYPE_MONTHLY_QUOTA_EXCEEDED = 'XmemoryMonthlyQuotaExceeded';
|
|
32
|
+
exports.TYPE_QUOTA_EXCEEDED = 'XmemoryQuotaExceeded';
|
|
33
|
+
exports.TYPE_AUTH_FAILED = 'XmemoryAuthFailed';
|
|
34
|
+
exports.TYPE_NOT_FOUND = 'XmemoryNotFound';
|
|
35
|
+
exports.TYPE_BAD_REQUEST = 'XmemoryBadRequest';
|
|
36
|
+
exports.TYPE_SCHEMA_REJECTED = 'XmemorySchemaRejected';
|
|
37
|
+
exports.TYPE_NOT_BOUND = 'XmemoryNotBound';
|
|
38
|
+
// Worker-side misconfiguration raised by activities.ts, kept distinct from
|
|
39
|
+
// NotBound because the remedy differs: one is a missing plugin registration, the
|
|
40
|
+
// other an activity scheduled with neither close timeout.
|
|
41
|
+
exports.TYPE_NO_DEADLINE = 'XmemoryNoDeadline';
|
|
42
|
+
// The Activity's deadline is already spent. Retryable: Temporal decides whether
|
|
43
|
+
// another attempt still fits, and failing here only avoids a doomed request.
|
|
44
|
+
exports.TYPE_DEADLINE_EXPIRED = 'XmemoryDeadlineExpired';
|
|
45
|
+
exports.TYPE_UNKNOWN = 'XmemoryUnknown';
|
|
46
|
+
// Re-export the durable-write outcome types (defined in the client-free leaf) so
|
|
47
|
+
// the full `type=` set is reachable from one module.
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `XmemoryPlugin` — the single line a Temporal user adds to their Worker.
|
|
3
|
+
*
|
|
4
|
+
* `configureWorker` injects the activities (and the auto-capture interceptor,
|
|
5
|
+
* if enabled); `runWorker` opens one xmemory client for the Worker's lifetime.
|
|
6
|
+
* Installs no data converter: one would rewrite every payload on the Worker,
|
|
7
|
+
* not just xmemory's.
|
|
8
|
+
*
|
|
9
|
+
* Use one plugin instance per Worker. The bound client lives in a per-plugin
|
|
10
|
+
* holder, so reusing one object across Workers is last-bind-wins.
|
|
11
|
+
*/
|
|
12
|
+
import type { Worker, WorkerOptions, WorkerPlugin } from '@temporalio/worker';
|
|
13
|
+
import { type XmemoryConfig } from './config';
|
|
14
|
+
import { type AutoCaptureConfig } from './interceptor';
|
|
15
|
+
export declare const PLUGIN_NAME = "xmemory";
|
|
16
|
+
export interface XmemoryPluginOptions {
|
|
17
|
+
/** Provide the API key in-process instead of via the env var. */
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
/** Enable auto-capture of activity results into memory. Off by default. */
|
|
20
|
+
autoCapture?: AutoCaptureConfig;
|
|
21
|
+
}
|
|
22
|
+
export declare class XmemoryPlugin implements WorkerPlugin {
|
|
23
|
+
#private;
|
|
24
|
+
readonly name = "xmemory";
|
|
25
|
+
constructor(config: XmemoryConfig, options?: XmemoryPluginOptions);
|
|
26
|
+
/** The in-process key, if one was supplied. Never serialized with the plugin. */
|
|
27
|
+
private apiKey;
|
|
28
|
+
configureWorker(options: WorkerOptions): WorkerOptions;
|
|
29
|
+
runWorker(worker: Worker, next: (w: Worker) => Promise<void>): Promise<void>;
|
|
30
|
+
private buildInstance;
|
|
31
|
+
}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `XmemoryPlugin` — the single line a Temporal user adds to their Worker.
|
|
4
|
+
*
|
|
5
|
+
* `configureWorker` injects the activities (and the auto-capture interceptor,
|
|
6
|
+
* if enabled); `runWorker` opens one xmemory client for the Worker's lifetime.
|
|
7
|
+
* Installs no data converter: one would rewrite every payload on the Worker,
|
|
8
|
+
* not just xmemory's.
|
|
9
|
+
*
|
|
10
|
+
* Use one plugin instance per Worker. The bound client lives in a per-plugin
|
|
11
|
+
* holder, so reusing one object across Workers is last-bind-wins.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.XmemoryPlugin = exports.PLUGIN_NAME = void 0;
|
|
15
|
+
const xmemory_1 = require("xmemory");
|
|
16
|
+
const activities_1 = require("./activities");
|
|
17
|
+
const config_1 = require("./config");
|
|
18
|
+
const defaults_1 = require("./defaults");
|
|
19
|
+
const interceptor_1 = require("./interceptor");
|
|
20
|
+
exports.PLUGIN_NAME = 'xmemory';
|
|
21
|
+
class XmemoryPlugin {
|
|
22
|
+
name = exports.PLUGIN_NAME;
|
|
23
|
+
// ECMAScript private fields throughout: TypeScript `private` is an ordinary
|
|
24
|
+
// enumerable property at runtime, so JSON.stringify(plugin) printed the API key,
|
|
25
|
+
// the endpoint, and anything an injected instance carried.
|
|
26
|
+
#holder = new activities_1.InstanceHolder();
|
|
27
|
+
#apiKey;
|
|
28
|
+
#options;
|
|
29
|
+
#config;
|
|
30
|
+
constructor(config, options = {}) {
|
|
31
|
+
this.#config = config;
|
|
32
|
+
const { apiKey, ...rest } = options;
|
|
33
|
+
this.#apiKey = apiKey;
|
|
34
|
+
this.#options = rest;
|
|
35
|
+
}
|
|
36
|
+
/** The in-process key, if one was supplied. Never serialized with the plugin. */
|
|
37
|
+
apiKey() {
|
|
38
|
+
return this.#apiKey;
|
|
39
|
+
}
|
|
40
|
+
configureWorker(options) {
|
|
41
|
+
const ourActivities = (0, activities_1.createActivities)(this.#holder, this.#config);
|
|
42
|
+
// Refused, not merged over: replacing an identically-named Activity would
|
|
43
|
+
// change what the caller's workflows execute. `hasOwn`, not `in`, so an
|
|
44
|
+
// Activity named `constructor` is not a false collision.
|
|
45
|
+
const taken = Object.keys(options.activities ?? {}).filter((name) => Object.hasOwn(ourActivities, name));
|
|
46
|
+
if (taken.length > 0) {
|
|
47
|
+
throw new Error(`xmemory plugin: the Worker already registers ${taken.join(', ')}. ` +
|
|
48
|
+
'Those names belong to this plugin; rename the conflicting activities.');
|
|
49
|
+
}
|
|
50
|
+
const activities = { ...(options.activities ?? {}), ...ourActivities };
|
|
51
|
+
// Only auto-capture registers an interceptor, and only when enabled.
|
|
52
|
+
if (!this.#options.autoCapture)
|
|
53
|
+
return { ...options, activities };
|
|
54
|
+
const autoCapture = this.#options.autoCapture;
|
|
55
|
+
const holder = this.#holder;
|
|
56
|
+
const config = this.#config;
|
|
57
|
+
const interceptors = {
|
|
58
|
+
...options.interceptors,
|
|
59
|
+
activityInbound: [
|
|
60
|
+
() => (0, interceptor_1.createAutoCaptureInterceptor)(holder, config, autoCapture),
|
|
61
|
+
...(options.interceptors?.activityInbound ?? []),
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
return { ...options, activities, interceptors };
|
|
65
|
+
}
|
|
66
|
+
async runWorker(worker, next) {
|
|
67
|
+
this.#holder.bind(this.#options.instance ?? this.buildInstance());
|
|
68
|
+
await next(worker);
|
|
69
|
+
}
|
|
70
|
+
buildInstance() {
|
|
71
|
+
const client = new xmemory_1.XmemoryClient({
|
|
72
|
+
apiKey: (0, config_1.resolveApiKey)(this.#config, this.apiKey()),
|
|
73
|
+
// Always explicit — see `resolveEndpoint`.
|
|
74
|
+
url: (0, config_1.resolveEndpoint)(this.#config),
|
|
75
|
+
// A fallback only: every activity overrides this per call.
|
|
76
|
+
timeoutMs: (0, defaults_1.clientTimeoutMs)(defaults_1.DEFAULT_TIMEOUTS.readMs, this.#config.clientMarginMs),
|
|
77
|
+
});
|
|
78
|
+
return client.instance(this.#config.instanceId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.XmemoryPlugin = XmemoryPlugin;
|