@panagos/jojoscript 0.7.1 → 0.9.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/README.md +809 -418
- package/bin/jojo.js +430 -170
- package/package.json +50 -59
- package/src/compiler.js +227 -186
- package/src/effect-parser.js +223 -0
- package/src/index.js +14 -0
- package/src/pipeline-hash.js +27 -0
- package/src/pipeline-ir.js +157 -0
- package/src/pipeline-parser.js +583 -418
- package/src/runtime/checkpoint-store.js +122 -0
- package/src/runtime/checkpoint.js +69 -0
- package/src/runtime/collections.js +467 -327
- package/src/runtime/effect-log.js +90 -0
- package/src/runtime/effects.js +263 -0
- package/src/runtime/errors.js +29 -0
- package/src/runtime/execution-context.js +86 -0
- package/src/runtime/index.js +70 -29
- package/src/runtime/inspect.js +62 -0
- package/src/runtime/metrics.js +41 -0
- package/src/runtime/retry.js +67 -0
- package/src/runtime/trace.js +66 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { NonRetryableError } from './errors.js'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Wraps `fn` so calling the returned function retries on failure, up to
|
|
5
|
+
* `options.attempts` total tries, with an optional fixed or exponential
|
|
6
|
+
* delay between attempts. Errors marked non-retryable (`NonRetryableError`,
|
|
7
|
+
* or any error with `error.retryable === false`) are never retried and
|
|
8
|
+
* propagate immediately.
|
|
9
|
+
*
|
|
10
|
+
* Used two ways by compiled pipelines:
|
|
11
|
+
* - fused into a preceding `map`/`mapAsync`/`filterAsync`/`tap` stage, so
|
|
12
|
+
* each *item* is retried independently: `map(items, retry(fetch, opts))`.
|
|
13
|
+
* - standalone, wrapping the whole pipeline expression built so far as a
|
|
14
|
+
* zero-argument thunk: `retry(() => (expr), opts)()`.
|
|
15
|
+
*/
|
|
16
|
+
export function retry(fn, options = {}, meta = {}) {
|
|
17
|
+
const attempts = options.attempts ?? 1
|
|
18
|
+
const backoff = options.backoff ?? 'fixed'
|
|
19
|
+
const delayMs = options.delay ?? 0
|
|
20
|
+
|
|
21
|
+
return async function retried(...args) {
|
|
22
|
+
let attempt = 1
|
|
23
|
+
|
|
24
|
+
while (true) {
|
|
25
|
+
try {
|
|
26
|
+
return await fn(...args)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof NonRetryableError || error?.retryable === false) {
|
|
29
|
+
annotate(error, meta, attempt)
|
|
30
|
+
throw error
|
|
31
|
+
}
|
|
32
|
+
if (attempt >= attempts) {
|
|
33
|
+
annotate(error, meta, attempt)
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const wait = backoff === 'exponential'
|
|
38
|
+
? delayMs * 2 ** (attempt - 1)
|
|
39
|
+
: delayMs
|
|
40
|
+
|
|
41
|
+
if (wait > 0) await sleep(wait)
|
|
42
|
+
attempt++
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sleep(ms) {
|
|
49
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* Attaches structured failure context (§16: pipelineId, nodeId, retryCount)
|
|
54
|
+
* to the error without changing its type/instanceof behavior, so the CLI
|
|
55
|
+
* (or user code) can report exactly which pipeline node exhausted retries.
|
|
56
|
+
*/
|
|
57
|
+
function annotate(error, meta, retryCount) {
|
|
58
|
+
if (error && typeof error === 'object') {
|
|
59
|
+
error.jojo = {
|
|
60
|
+
pipelineId: meta.pipelineId ?? null,
|
|
61
|
+
nodeId: meta.id ?? null,
|
|
62
|
+
operation: 'retry',
|
|
63
|
+
retryCount,
|
|
64
|
+
timestamp: new Date().toISOString()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getCurrentContext, getDefaultContext } from './execution-context.js'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Generic per-node instrumentation used only by `jojo profile` (the
|
|
5
|
+
* compiler wraps every stage's output with a call to this function when
|
|
6
|
+
* compiled with `{ profile: true }`). It must be transparent to *type*,
|
|
7
|
+
* not just to values: eager results (arrays, Maps, Sets, scalars) are
|
|
8
|
+
* counted and returned completely unchanged, while lazy
|
|
9
|
+
* generator/async-generator results are wrapped in an equivalent
|
|
10
|
+
* generator that counts items as they're pulled through — never
|
|
11
|
+
* materializing anything itself.
|
|
12
|
+
*/
|
|
13
|
+
export function traceNode(value, meta = {}) {
|
|
14
|
+
const ctx = getCurrentContext() ?? getDefaultContext()
|
|
15
|
+
const start = process.hrtime.bigint()
|
|
16
|
+
|
|
17
|
+
function finish(count) {
|
|
18
|
+
const durationMs = Number(process.hrtime.bigint() - start) / 1e6
|
|
19
|
+
ctx.metrics.record(meta.id, meta.type, count, durationMs)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isAsyncGenerator = value != null &&
|
|
23
|
+
typeof value.next === 'function' &&
|
|
24
|
+
typeof value[Symbol.asyncIterator] === 'function'
|
|
25
|
+
|
|
26
|
+
const isSyncGenerator = !isAsyncGenerator && value != null &&
|
|
27
|
+
typeof value.next === 'function' &&
|
|
28
|
+
typeof value[Symbol.iterator] === 'function'
|
|
29
|
+
|
|
30
|
+
if (isAsyncGenerator) {
|
|
31
|
+
return (async function* () {
|
|
32
|
+
let count = 0
|
|
33
|
+
try {
|
|
34
|
+
for await (const item of value) {
|
|
35
|
+
count++
|
|
36
|
+
yield item
|
|
37
|
+
}
|
|
38
|
+
} finally {
|
|
39
|
+
finish(count)
|
|
40
|
+
}
|
|
41
|
+
})()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isSyncGenerator) {
|
|
45
|
+
return (function* () {
|
|
46
|
+
let count = 0
|
|
47
|
+
try {
|
|
48
|
+
for (const item of value) {
|
|
49
|
+
count++
|
|
50
|
+
yield item
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
finish(count)
|
|
54
|
+
}
|
|
55
|
+
})()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const count = Array.isArray(value)
|
|
59
|
+
? value.length
|
|
60
|
+
: value instanceof Map || value instanceof Set
|
|
61
|
+
? value.size
|
|
62
|
+
: 1
|
|
63
|
+
|
|
64
|
+
finish(count)
|
|
65
|
+
return value
|
|
66
|
+
}
|