@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.
@@ -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
+ }