@requence/service 0.8.2-edge.1156 → 0.8.2-edge.1159
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -0
- package/build/index.js +289 -50
- package/build/index.js.map +13 -11
- package/build/types/helpers/src/protocol/index.d.ts +1 -0
- package/build/types/helpers/src/protocol/index.d.ts.map +1 -1
- package/build/types/helpers/src/protocol/logicExecutionTime.d.ts +22 -0
- package/build/types/helpers/src/protocol/logicExecutionTime.d.ts.map +1 -0
- package/build/types/helpers/src/utils/requenceCallback.d.ts +153 -3
- package/build/types/helpers/src/utils/requenceCallback.d.ts.map +1 -1
- package/build/types/service/src/boundary.d.ts +9 -0
- package/build/types/service/src/boundary.d.ts.map +1 -1
- package/build/types/service/src/callbackGrowth.d.ts +102 -0
- package/build/types/service/src/callbackGrowth.d.ts.map +1 -0
- package/build/types/service/src/createAmqpConnection.d.ts +1 -0
- package/build/types/service/src/createAmqpConnection.d.ts.map +1 -1
- package/build/types/service/src/durableCallback.d.ts +223 -0
- package/build/types/service/src/durableCallback.d.ts.map +1 -0
- package/build/types/service/src/helpers.d.ts +6 -5
- package/build/types/service/src/helpers.d.ts.map +1 -1
- package/build/types/service/src/index.d.ts +1 -0
- package/build/types/service/src/index.d.ts.map +1 -1
- package/build/types/service/src/surfaceHandler.d.ts.map +1 -1
- package/build/types/service/src/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -255,6 +255,169 @@ LOCKED`, a Redis `SET NX`, one designated attacher), and only act on keys you ho
|
|
|
255
255
|
node's [surface](#surfaces--a-ui-of-your-own-on-the-node) — see
|
|
256
256
|
[`api.render`](#apirenderui-props-options--rendering-from-act).
|
|
257
257
|
|
|
258
|
+
## Callbacks — functions you emit alongside your data
|
|
259
|
+
|
|
260
|
+
A function in what a service emits arrives downstream as a function the graph may
|
|
261
|
+
call back. An email receiver yields a mail *and* a `delete()` a later node may or
|
|
262
|
+
may not use.
|
|
263
|
+
|
|
264
|
+
This section is the reference for callback behaviour in every SDK — the Python SDK
|
|
265
|
+
(`python/README.md`) speaks the same wire and states only where it differs. The
|
|
266
|
+
design authority is
|
|
267
|
+
[`knowledge/durable-callbacks.md`](../../knowledge/durable-callbacks.md).
|
|
268
|
+
|
|
269
|
+
### Where you write the function decides how long it lives
|
|
270
|
+
|
|
271
|
+
There are two kinds, and you pick by **where you write it** — the same rule as
|
|
272
|
+
[handler props](#handler-props--where-you-write-the-function-decides-how-long-it-can-be-pressed),
|
|
273
|
+
stated for a second mechanism:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { createDurableCallback, createService } from '@requence/service'
|
|
277
|
+
|
|
278
|
+
// DURABLE. Declared ONCE at module scope, addressed by the name it is given. It
|
|
279
|
+
// closes over nothing per mail, so any instance of this version can run it —
|
|
280
|
+
// this one, or the one that replaces it in a redeploy.
|
|
281
|
+
const deleteMail = createDurableCallback<null, { uid: number }, 'receiver'>(
|
|
282
|
+
'deleteMail',
|
|
283
|
+
async (_arg, { uid, configuration }) => {
|
|
284
|
+
const client = await connect(configuration) // rebuilt, never captured
|
|
285
|
+
await client.messageDelete([uid], { uid: true })
|
|
286
|
+
client.close()
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
createService('receiver', async function* () {
|
|
291
|
+
for await (const mail of mails) {
|
|
292
|
+
yield {
|
|
293
|
+
subject: mail.subject,
|
|
294
|
+
// A reference, not a closure: one registration, however many mails.
|
|
295
|
+
delete: deleteMail.for({ uid: mail.uid }),
|
|
296
|
+
// CONNECTION-BOUND. Needs the LIVE imap client, so it correctly dies with
|
|
297
|
+
// this message — and holds everything it captured until then.
|
|
298
|
+
stream: () => imap.download(mail.uid),
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Write it inline when it genuinely needs the live process** — an open stream, a
|
|
305
|
+
client the handler is mid-conversation with. **Declare it with
|
|
306
|
+
`createDurableCallback` when it needs only data**, and bind that data with
|
|
307
|
+
`.for(…)`.
|
|
308
|
+
|
|
309
|
+
A connection-bound function is kept in the connection's registry until the message
|
|
310
|
+
**settles**, which for a CONTINUOUS handler is until the task *ends*. So a generator
|
|
311
|
+
minting one per emission grows by one closure per emission for the life of the run,
|
|
312
|
+
and each closure holds on to everything that was in scope when it was written — the
|
|
313
|
+
whole mail, not just the uid. It is also why a connection-bound callback is only
|
|
314
|
+
useful from a generator handler: a LINEAR handler's registrations go the moment it
|
|
315
|
+
returns, so the reference it emitted is dead before anything downstream sees it. A
|
|
316
|
+
durable one registers nothing, so a LINEAR node can emit one and a later node can
|
|
317
|
+
still call it.
|
|
318
|
+
|
|
319
|
+
### What a durable callback is handed
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
createDurableCallback<Arg, Bound, Configuration>(name, (arg, context) => …)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
| | |
|
|
326
|
+
|---|---|
|
|
327
|
+
| `arg` | what the graph passed when it called the callback |
|
|
328
|
+
| `context.…` | whatever `.for({ … })` bound to this emission, spread |
|
|
329
|
+
| `context.configuration` | the node's **static** configuration |
|
|
330
|
+
|
|
331
|
+
**And that is all of it.** No message, no task, no connection — that is exactly what
|
|
332
|
+
makes it durable: it may run in an instance that never saw the emission, started
|
|
333
|
+
after the emitting process died. So anything it needs it **rebuilds** rather than
|
|
334
|
+
captures, and `configuration` is what it rebuilds from.
|
|
335
|
+
|
|
336
|
+
The three type arguments are the three things the two sides disagree about, and
|
|
337
|
+
`Configuration` takes either a type or the **name of the service version** to look it
|
|
338
|
+
up from (`ConfigurationOf<'receiver'>`). **Prefer the version-name form**: that type
|
|
339
|
+
is generated into `requence-env.d.ts` beside the version it belongs to, so naming the
|
|
340
|
+
version is the only form that cannot drift from what the node actually sends.
|
|
341
|
+
|
|
342
|
+
A binding may not be:
|
|
343
|
+
|
|
344
|
+
- **a scalar.** The bound data *is* the context and it is spread, so a string would
|
|
345
|
+
quietly become `{ 0: 'd', 1: 'e', … }`.
|
|
346
|
+
- **a `configuration` key.** The node's own configuration has that name, and a
|
|
347
|
+
precedence rule either way would mean your data or the node's credentials silently
|
|
348
|
+
disappearing.
|
|
349
|
+
|
|
350
|
+
Both are refused where you wrote them, and again when an invocation is unpacked.
|
|
351
|
+
|
|
352
|
+
A declaration with nothing to bind is emitted as itself — `.for()` is optional there,
|
|
353
|
+
and TypeScript only lets you leave it off when there is genuinely nothing to bind:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
const acknowledge = createDurableCallback<null>('acknowledge', () => ack())
|
|
357
|
+
|
|
358
|
+
yield { subject: mail.subject, acknowledge }
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**A callback that takes no argument is `RequenceCallback<null>`, not `<void>`** —
|
|
362
|
+
both here and in the output schema the position is declared in. `void` has no root
|
|
363
|
+
type a JSON schema can hold, so the schema cannot be authored at all, and
|
|
364
|
+
`undefined` compiles but silently drops the `callback` marker along with the
|
|
365
|
+
argument, leaving a plain object where the reference has to survive validation. A
|
|
366
|
+
graph then calls it as `context.input.delete(null)`.
|
|
367
|
+
|
|
368
|
+
**Two declarations under the same name throw at registration**, for the reason a
|
|
369
|
+
duplicate `createUI` id does: the name is the whole address, so an ambiguous one is an
|
|
370
|
+
invocation reaching the *wrong* handler — which reads like a bug in the handler — and
|
|
371
|
+
that is worse than a service that refuses to start.
|
|
372
|
+
|
|
373
|
+
### An invocation waits out a redeploy, not a weekend
|
|
374
|
+
|
|
375
|
+
Consuming the invocations is automatic. The queue is
|
|
376
|
+
`service-<name>@<version>-callbacks`, one per version, created by Requence when the
|
|
377
|
+
version is provisioned and asserted again by the SDK on connect. Whichever instance is
|
|
378
|
+
free serves it, so a rolling restart is invisible to the graph.
|
|
379
|
+
|
|
380
|
+
It is **not a mailbox**: an invocation expires after ~60s, and an expired or
|
|
381
|
+
unroutable one is reported on the run — a system note on the node that emitted the
|
|
382
|
+
callback, saying whether the version has nothing running or is no longer deployed at
|
|
383
|
+
all. A callback invoked today and run whenever somebody next deploys would be a silent
|
|
384
|
+
effect, and nothing checks that a service is up before a graph calls
|
|
385
|
+
`context.input.delete()`.
|
|
386
|
+
|
|
387
|
+
A name this build does not declare, and a handler that throws, are said out loud in
|
|
388
|
+
this process's log — the graph called the callback and moved on, so there is no
|
|
389
|
+
request left to fail. The invocation is **acked either way**: a requeue would rerun a
|
|
390
|
+
side-effecting callback forever.
|
|
391
|
+
|
|
392
|
+
### The pile-up warning
|
|
393
|
+
|
|
394
|
+
If connection-bound callbacks accumulate, the SDK tells you in its **own log** — once
|
|
395
|
+
when the registry crosses a threshold, then at each power of ten — naming the `yield`
|
|
396
|
+
positions doing it and the fix:
|
|
397
|
+
|
|
398
|
+
| Where | First warning | Then |
|
|
399
|
+
|---|---|---|
|
|
400
|
+
| a dev session (dev overlay) | 100 | ×10 |
|
|
401
|
+
| a deployed version | 1 000 | ×10 (10k, 100k, …) |
|
|
402
|
+
|
|
403
|
+
A dev session warns ten times earlier on purpose: a hundred emissions in `bun run dev`
|
|
404
|
+
already means a generator is minting closures in a loop, and catching it there is worth
|
|
405
|
+
far more than catching it in production. Two counters feed it — this delivery's, which
|
|
406
|
+
identifies the one generator, and the whole connection's, which is what actually
|
|
407
|
+
threatens the process when many tasks each stay under their own threshold.
|
|
408
|
+
|
|
409
|
+
It is a **log line for you**, not a task event: you are the only person who can act on
|
|
410
|
+
it. Nothing is capped and nothing is evicted — a cap would kill a legitimate long-lived
|
|
411
|
+
handle at an arbitrary number you cannot exempt it from.
|
|
412
|
+
|
|
413
|
+
### Two gaps
|
|
414
|
+
|
|
415
|
+
- **Computed configuration is not available to a durable callback**, and cannot be:
|
|
416
|
+
config functions are evaluated per dispatch, and this call belongs to none. A node
|
|
417
|
+
whose credentials are computed cannot serve one.
|
|
418
|
+
- **A logic node's `createCallback` global is connection-bound only.** There is no
|
|
419
|
+
durable form inside a script.
|
|
420
|
+
|
|
258
421
|
## Surfaces — a UI of your own on the node
|
|
259
422
|
|
|
260
423
|
A service can ship its own UI component and have Requence render it **on the
|