@ambarltd/core 0.1.17
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 +10 -0
- package/dist/callable.d.ts +1 -0
- package/dist/callable.js +6 -0
- package/dist/future.d.ts +102 -0
- package/dist/future.js +164 -0
- package/dist/helpers/object.d.ts +9 -0
- package/dist/helpers/object.js +31 -0
- package/dist/json/decoder.d.ts +99 -0
- package/dist/json/decoder.js +213 -0
- package/dist/json/encoder.d.ts +50 -0
- package/dist/json/encoder.js +115 -0
- package/dist/json/schema.d.ts +78 -0
- package/dist/json/schema.js +168 -0
- package/dist/json/types.d.ts +6 -0
- package/dist/json/types.js +1 -0
- package/dist/list.d.ts +35 -0
- package/dist/list.js +130 -0
- package/dist/maybe.d.ts +104 -0
- package/dist/maybe.js +106 -0
- package/dist/remote-data.d.ts +117 -0
- package/dist/remote-data.js +148 -0
- package/dist/result.d.ts +75 -0
- package/dist/result.js +126 -0
- package/dist/router.d.ts +117 -0
- package/dist/router.js +106 -0
- package/dist/test.d.ts +63 -0
- package/dist/test.js +470 -0
- package/dist/time.d.ts +118 -0
- package/dist/time.js +387 -0
- package/dist/tracing/opentelemetry.d.ts +27 -0
- package/dist/tracing/opentelemetry.js +215 -0
- package/dist/tracing/proxy.d.ts +20 -0
- package/dist/tracing/proxy.js +103 -0
- package/dist/tracing/simple.d.ts +10 -0
- package/dist/tracing/simple.js +224 -0
- package/dist/tracing.d.ts +29 -0
- package/dist/tracing.js +55 -0
- package/dist/trampoline.d.ts +24 -0
- package/dist/trampoline.js +46 -0
- package/dist/tree-map.d.ts +73 -0
- package/dist/tree-map.js +169 -0
- package/dist/tree-set.d.ts +63 -0
- package/dist/tree-set.js +114 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// A Tracer that wraps another Tracer and forwards only the traces whose name
|
|
2
|
+
// passes the configured filters. Non-matching traces run untraced — the work
|
|
3
|
+
// still executes, but no span is created.
|
|
4
|
+
//
|
|
5
|
+
// The match is decided once per call and applies to the whole span (delegate the
|
|
6
|
+
// call wholesale, or bypass it wholesale), so a span is never half-emitted.
|
|
7
|
+
//
|
|
8
|
+
// Optionally (config.includeCallSite) it tags each forwarded trace with the
|
|
9
|
+
// source file and line it was called from, as `code.filepath` / `code.lineno`.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// setTracer(new ProxyTracer({
|
|
13
|
+
// tracer: new OpenTelemetryTracer({ ... }),
|
|
14
|
+
// traceFilter: name => /^mongo\./.test(name) || name.includes("idempotency"),
|
|
15
|
+
// includeCallSite: true,
|
|
16
|
+
// }));
|
|
17
|
+
export { ProxyTracer };
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { dirname, sep } from "node:path";
|
|
20
|
+
class ProxyTracer {
|
|
21
|
+
inner;
|
|
22
|
+
// The name predicate, or `undefined` for "no filtering — forward all".
|
|
23
|
+
traceFilter;
|
|
24
|
+
includeCallSite;
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.inner = config.tracer;
|
|
27
|
+
this.traceFilter = config.traceFilter;
|
|
28
|
+
this.includeCallSite = config.includeCallSite ?? false;
|
|
29
|
+
}
|
|
30
|
+
// Whether a trace with this name should be forwarded to the wrapped tracer.
|
|
31
|
+
// No filter → forward everything; otherwise the predicate decides.
|
|
32
|
+
enabled(name) {
|
|
33
|
+
return this.traceFilter === undefined || this.traceFilter(name);
|
|
34
|
+
}
|
|
35
|
+
// Merge in the caller's source location when configured. Only called on the
|
|
36
|
+
// forwarding path, so the stack-walk cost is paid only for traces we keep.
|
|
37
|
+
withCallSite(attributes) {
|
|
38
|
+
if (!this.includeCallSite) {
|
|
39
|
+
return attributes;
|
|
40
|
+
}
|
|
41
|
+
const site = callSite();
|
|
42
|
+
return site === undefined ? attributes : { ...attributes, ...site };
|
|
43
|
+
}
|
|
44
|
+
trace(name, attributes, f) {
|
|
45
|
+
if (!this.enabled(name)) {
|
|
46
|
+
return f();
|
|
47
|
+
}
|
|
48
|
+
return this.inner.trace(name, this.withCallSite(attributes), f);
|
|
49
|
+
}
|
|
50
|
+
traceP(name, attributes, f) {
|
|
51
|
+
if (!this.enabled(name)) {
|
|
52
|
+
return f();
|
|
53
|
+
}
|
|
54
|
+
return this.inner.traceP(name, this.withCallSite(attributes), f);
|
|
55
|
+
}
|
|
56
|
+
traceF(name, attributes, f) {
|
|
57
|
+
if (!this.enabled(name)) {
|
|
58
|
+
return f;
|
|
59
|
+
}
|
|
60
|
+
return this.inner.traceF(name, this.withCallSite(attributes), f);
|
|
61
|
+
}
|
|
62
|
+
// Events are always forwarded: the wrapped tracer attaches them to the active
|
|
63
|
+
// span if one exists and no-ops otherwise, so an event naturally lands on its
|
|
64
|
+
// enclosing span when that span passed the filter, and is dropped when it did
|
|
65
|
+
// not (no span is active to attach to).
|
|
66
|
+
event(name, attributes) {
|
|
67
|
+
this.inner.event(name, attributes);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Frames inside this directory (the tracing facade + tracers) are skipped when
|
|
71
|
+
// looking for the caller's location.
|
|
72
|
+
const TRACING_PREFIX = dirname(fileURLToPath(import.meta.url)) + sep;
|
|
73
|
+
// Only need to see past the handful of tracing frames to reach the caller.
|
|
74
|
+
const STACK_LIMIT = 12;
|
|
75
|
+
// The source location of the first stack frame outside the tracing library, as
|
|
76
|
+
// OpenTelemetry `code.*` attributes. Undefined if it can't be determined.
|
|
77
|
+
function callSite() {
|
|
78
|
+
const previousLimit = Error.stackTraceLimit;
|
|
79
|
+
Error.stackTraceLimit = STACK_LIMIT; // only need one frame past ours; keep it cheap
|
|
80
|
+
const holder = {};
|
|
81
|
+
Error.captureStackTrace(holder);
|
|
82
|
+
Error.stackTraceLimit = previousLimit;
|
|
83
|
+
for (const line of holder.stack?.split("\n").slice(1) ?? []) {
|
|
84
|
+
const frame = parseFrame(line);
|
|
85
|
+
if (frame === undefined || frame.file.startsWith(TRACING_PREFIX)) {
|
|
86
|
+
continue; // skip non-frames and the tracing library's own frames
|
|
87
|
+
}
|
|
88
|
+
return { "code.filepath": frame.file, "code.lineno": frame.line };
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
// Pull "file:line:col" out of a V8 stack frame line, with or without the
|
|
93
|
+
// "at fn (...)" wrapper.
|
|
94
|
+
function parseFrame(line) {
|
|
95
|
+
const match = line.match(/\((.+):(\d+):(\d+)\)\s*$/) ?? line.match(/at\s+(?:async\s+)?(.+):(\d+):(\d+)\s*$/);
|
|
96
|
+
const path = match?.[1];
|
|
97
|
+
const lineno = match?.[2];
|
|
98
|
+
if (path === undefined || lineno === undefined) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const file = path.startsWith("file://") ? fileURLToPath(path) : path;
|
|
102
|
+
return { file, line: Number(lineno) };
|
|
103
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { SimpleTracer };
|
|
2
|
+
import type { Tracer, Attributes } from "../tracing";
|
|
3
|
+
import { Future } from "../future";
|
|
4
|
+
declare class SimpleTracer implements Tracer {
|
|
5
|
+
constructor();
|
|
6
|
+
trace<A>(name: string, attributes: Attributes, f: () => A): A;
|
|
7
|
+
traceP<A>(name: string, attributes: Attributes, f: () => Promise<A>): Promise<A>;
|
|
8
|
+
traceF<E, A>(name: string, attributes: Attributes, f: Future<E, A>): Future<E, A>;
|
|
9
|
+
event(name: string, attributes?: Attributes): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// This module allows for the issuing of tracing events which can be seen
|
|
2
|
+
// in a tracing explorer like https://ui.perfetto.dev/.
|
|
3
|
+
//
|
|
4
|
+
// It works by emitting the traces to stdout. You should then pipe stdout into a
|
|
5
|
+
// JSON file, filtering only the lines that contain trace events.
|
|
6
|
+
//
|
|
7
|
+
// It uses AsyncLocalStorage to nest traces, so there is no need to pass a token
|
|
8
|
+
// object around and it detects nested traces through promises.
|
|
9
|
+
//
|
|
10
|
+
// Collect events with:
|
|
11
|
+
// $ docker logs event-sourcing-backend | grep "\"ph\"" | jq -s '.' >events.json
|
|
12
|
+
export { SimpleTracer };
|
|
13
|
+
import { Future } from "../future";
|
|
14
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
|
+
// The currently-active trace. Nested `trace`/`traceP`/`traceF` calls read this
|
|
16
|
+
// to find their parent, so nesting needs no token to be threaded through code.
|
|
17
|
+
const store = new AsyncLocalStorage();
|
|
18
|
+
// Begin a trace as a child of the active one, or a new root when none is active.
|
|
19
|
+
function begin(name, attributes) {
|
|
20
|
+
const parent = store.getStore();
|
|
21
|
+
return parent ? parent.startSubtrace(name, attributes) : Trace.start(name, attributes);
|
|
22
|
+
}
|
|
23
|
+
// Run `fn` with `t` as the active trace; with `undefined`, run with no active
|
|
24
|
+
// trace (used to restore the parent — which may be a root — when settling).
|
|
25
|
+
function runIn(t, fn) {
|
|
26
|
+
return t ? store.run(t, fn) : store.exit(fn);
|
|
27
|
+
}
|
|
28
|
+
// Emits Perfetto/Chrome-event JSON to stdout, nesting traces via the
|
|
29
|
+
// module-level AsyncLocalStorage `store` so no token is threaded through code.
|
|
30
|
+
class SimpleTracer {
|
|
31
|
+
constructor() { }
|
|
32
|
+
// Trace a synchronous function.
|
|
33
|
+
trace(name, attributes, f) {
|
|
34
|
+
const t = begin(name, attributes);
|
|
35
|
+
try {
|
|
36
|
+
return store.run(t, f);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
t.end();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Trace an asynchronous function. `store.run` keeps the trace active across the
|
|
43
|
+
// awaits inside `f` (AsyncLocalStorage propagates it), so traces created within
|
|
44
|
+
// `f` nest under it. After the awaited promise settles we are back in the
|
|
45
|
+
// caller's context, so the trace ends in the right place and a subsequent
|
|
46
|
+
// `traceP` becomes a sibling.
|
|
47
|
+
async traceP(name, attributes, f) {
|
|
48
|
+
const t = begin(name, attributes);
|
|
49
|
+
try {
|
|
50
|
+
return await store.run(t, f);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
t.end();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Trace a Future.
|
|
57
|
+
// N.B. Forking here is fine. It's what `bracket` and `chainRej` do under the hood too.
|
|
58
|
+
traceF(name, attributes, f) {
|
|
59
|
+
return Future.create((reject, resolve) => {
|
|
60
|
+
const parent = store.getStore();
|
|
61
|
+
const t = parent ? parent.startSubtrace(name, attributes) : Trace.start(name, attributes);
|
|
62
|
+
return runIn(t, () => f.fork(err => {
|
|
63
|
+
t.end();
|
|
64
|
+
runIn(parent, () => reject(err));
|
|
65
|
+
}, val => {
|
|
66
|
+
t.end();
|
|
67
|
+
runIn(parent, () => resolve(val));
|
|
68
|
+
}));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// Record a point-in-time (instant) event on the active trace's lane. Renders
|
|
72
|
+
// as a marker on that track in Perfetto. No-op when no trace is active.
|
|
73
|
+
event(name, attributes = {}) {
|
|
74
|
+
const t = store.getStore();
|
|
75
|
+
if (!t) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
emit({ name, ph: PhaseInstant, ts: nowMicros(), pid: PROCESS_ID, tid: t.threadId, s: "t", args: attributes });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const PROCESS_ID = 1;
|
|
82
|
+
// Current time in microseconds from a monotonic, high-resolution clock.
|
|
83
|
+
// `process.hrtime.bigint()` returns nanoseconds, so we divide by 1000. This
|
|
84
|
+
// avoids the millisecond-granularity collisions of a wall-clock (Date/POSIX)
|
|
85
|
+
// timestamp, which collapsed nested spans to zero width.
|
|
86
|
+
function nowMicros() {
|
|
87
|
+
return Number(process.hrtime.bigint() / 1000n);
|
|
88
|
+
}
|
|
89
|
+
// A pool of thread ids. A root trace acquires one for its lifetime and releases
|
|
90
|
+
// it on end, so concurrent requests always get distinct lanes while the total
|
|
91
|
+
// number of lanes stays as small as the peak concurrency (rather than growing
|
|
92
|
+
// once per request). Node runs JavaScript on a single thread, so no lock is
|
|
93
|
+
// required (unlike the Python implementation).
|
|
94
|
+
class ThreadIdPool {
|
|
95
|
+
free = [];
|
|
96
|
+
next = 0;
|
|
97
|
+
named = new Set();
|
|
98
|
+
acquire() {
|
|
99
|
+
const id = this.free.pop() ?? this.next++;
|
|
100
|
+
if (!this.named.has(id)) {
|
|
101
|
+
this.named.add(id);
|
|
102
|
+
emitThreadName(id, `request lane ${id}`);
|
|
103
|
+
}
|
|
104
|
+
return id;
|
|
105
|
+
}
|
|
106
|
+
release(id) {
|
|
107
|
+
this.free.push(id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const threadPool = new ThreadIdPool();
|
|
111
|
+
// Trace event phases — synchronous Duration events plus Metadata.
|
|
112
|
+
// See https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
|
|
113
|
+
const PhaseDurationBegin = "B"; // Enter a duration on this thread
|
|
114
|
+
const PhaseDurationEnd = "E"; // Leave the innermost open duration on this thread
|
|
115
|
+
const PhaseInstant = "i"; // A point-in-time event on this thread
|
|
116
|
+
const PhaseMetadata = "M"; // Metadata (thread/process names)
|
|
117
|
+
/**
|
|
118
|
+
* An ongoing trace, occupying a track (thread) in the trace explorer.
|
|
119
|
+
*
|
|
120
|
+
* A root trace (created with `Trace.start`) owns a thread id for its lifetime.
|
|
121
|
+
* Subtraces share that thread id so they nest, by stack order, within the root.
|
|
122
|
+
* Because Duration events must be strictly nested per thread, callers must end
|
|
123
|
+
* subtraces before the parent ends — which `subtrace` guarantees.
|
|
124
|
+
*/
|
|
125
|
+
class Trace {
|
|
126
|
+
name;
|
|
127
|
+
threadId;
|
|
128
|
+
ownsThread;
|
|
129
|
+
args;
|
|
130
|
+
constructor(name, threadId,
|
|
131
|
+
// Only a root trace owns its thread id and releases it back to the pool on
|
|
132
|
+
// end. Subtraces borrow the root's thread id.
|
|
133
|
+
ownsThread, args) {
|
|
134
|
+
this.name = name;
|
|
135
|
+
this.threadId = threadId;
|
|
136
|
+
this.ownsThread = ownsThread;
|
|
137
|
+
this.args = args;
|
|
138
|
+
}
|
|
139
|
+
// Start a trace on its own thread (track).
|
|
140
|
+
static start(name, args = {}) {
|
|
141
|
+
const trace = new Trace(name, threadPool.acquire(), true, args);
|
|
142
|
+
trace.begin();
|
|
143
|
+
return trace;
|
|
144
|
+
}
|
|
145
|
+
// Trace a subsection of the original trace, nested on the same thread.
|
|
146
|
+
startSubtrace(name, // trace name
|
|
147
|
+
args = {}) {
|
|
148
|
+
const trace = new Trace(name, this.threadId, false, args);
|
|
149
|
+
trace.begin();
|
|
150
|
+
return trace;
|
|
151
|
+
}
|
|
152
|
+
end() {
|
|
153
|
+
emit({
|
|
154
|
+
name: this.name,
|
|
155
|
+
ph: PhaseDurationEnd,
|
|
156
|
+
ts: nowMicros(),
|
|
157
|
+
pid: PROCESS_ID,
|
|
158
|
+
tid: this.threadId,
|
|
159
|
+
args: {},
|
|
160
|
+
});
|
|
161
|
+
if (this.ownsThread) {
|
|
162
|
+
threadPool.release(this.threadId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
begin() {
|
|
166
|
+
emit({
|
|
167
|
+
name: this.name,
|
|
168
|
+
ph: PhaseDurationBegin,
|
|
169
|
+
ts: nowMicros(),
|
|
170
|
+
pid: PROCESS_ID,
|
|
171
|
+
tid: this.threadId,
|
|
172
|
+
args: this.args,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// Run `body` within a nested trace, ending the subtrace afterwards even if
|
|
176
|
+
// `body` throws. This is the idiomatic TypeScript equivalent of the Python
|
|
177
|
+
// `subtrace` async context manager.
|
|
178
|
+
//
|
|
179
|
+
// await trace.subtrace("my-step", async (sub) => {
|
|
180
|
+
// ... // do work here.
|
|
181
|
+
// });
|
|
182
|
+
//
|
|
183
|
+
async subtrace(name, body, args = {}) {
|
|
184
|
+
const trace = this.startSubtrace(name, args);
|
|
185
|
+
try {
|
|
186
|
+
return await body(trace);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
trace.end();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
let processNamed = false;
|
|
194
|
+
// Name the process once, lazily, so the explorer shows a readable label.
|
|
195
|
+
function ensureProcessName() {
|
|
196
|
+
if (processNamed) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
processNamed = true;
|
|
200
|
+
emit({
|
|
201
|
+
name: "process_name",
|
|
202
|
+
ph: PhaseMetadata,
|
|
203
|
+
ts: 0,
|
|
204
|
+
pid: PROCESS_ID,
|
|
205
|
+
tid: 0,
|
|
206
|
+
args: { name: "backend" },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// Name a thread (lane) once, the first time it is used.
|
|
210
|
+
function emitThreadName(tid, name) {
|
|
211
|
+
ensureProcessName();
|
|
212
|
+
emit({
|
|
213
|
+
name: "thread_name",
|
|
214
|
+
ph: PhaseMetadata,
|
|
215
|
+
ts: 0,
|
|
216
|
+
pid: PROCESS_ID,
|
|
217
|
+
tid,
|
|
218
|
+
args: { name },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// Emit a trace event as a JSON line on stdout.
|
|
222
|
+
function emit(event) {
|
|
223
|
+
console.log(JSON.stringify(event));
|
|
224
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { type Tracer, type Attributes, type Name, setTracer, trace, traceP, traceF, event };
|
|
2
|
+
import type { Future } from "./future";
|
|
3
|
+
type Attributes = Record<string, string | number | boolean>;
|
|
4
|
+
type Name = string | (Attributes & {
|
|
5
|
+
name: string;
|
|
6
|
+
});
|
|
7
|
+
interface Tracer {
|
|
8
|
+
/**
|
|
9
|
+
* Trace a synchronous call.
|
|
10
|
+
*/
|
|
11
|
+
trace<A>(name: string, attributes: Attributes, f: () => A): A;
|
|
12
|
+
/**
|
|
13
|
+
* Trace an asynchronous call.
|
|
14
|
+
*/
|
|
15
|
+
traceP<A>(name: string, attributes: Attributes, f: () => Promise<A>): Promise<A>;
|
|
16
|
+
/**
|
|
17
|
+
* Trace a Future
|
|
18
|
+
*/
|
|
19
|
+
traceF<E, A>(name: string, attributes: Attributes, f: Future<E, A>): Future<E, A>;
|
|
20
|
+
/**
|
|
21
|
+
* Record a point-in-time event on the currently-active trace.
|
|
22
|
+
*/
|
|
23
|
+
event(name: string, attributes?: Attributes): void;
|
|
24
|
+
}
|
|
25
|
+
declare function setTracer(tracer: Tracer): void;
|
|
26
|
+
declare function trace<A>(name: Name, f: () => A): A;
|
|
27
|
+
declare function traceP<A>(name: Name, f: () => Promise<A>): Promise<A>;
|
|
28
|
+
declare function traceF<E, A>(name: Name, f: Future<E, A>): Future<E, A>;
|
|
29
|
+
declare function event(name: string, attributes?: Attributes): void;
|
package/dist/tracing.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Tracing facade.
|
|
2
|
+
//
|
|
3
|
+
// The application imports `trace`, `traceP`, `traceF` and `traceRequests` from
|
|
4
|
+
// here. The first three delegate to the currently-selected `Tracer`, so the
|
|
5
|
+
// whole program's tracing destination is switched at runtime with `setTracer`.
|
|
6
|
+
//
|
|
7
|
+
// There is NO tracer by default: Use `setTracer` to set one.
|
|
8
|
+
//
|
|
9
|
+
export { setTracer, trace, traceP, traceF, event };
|
|
10
|
+
// The active tracer. None by default — traces are no-ops until one is set.
|
|
11
|
+
let current = undefined;
|
|
12
|
+
// Choose where traces go. Affects every subsequent trace call across the program.
|
|
13
|
+
function setTracer(tracer) {
|
|
14
|
+
current = tracer;
|
|
15
|
+
}
|
|
16
|
+
// The delegating API. These read `current` at call time, so a `setTracer` switch
|
|
17
|
+
// takes effect immediately for all callers. With no tracer set, they run the
|
|
18
|
+
// work untraced. The name argument may be a plain string or an attributes object
|
|
19
|
+
// carrying the name (see `Name`).
|
|
20
|
+
function trace(name, f) {
|
|
21
|
+
if (!current) {
|
|
22
|
+
return f();
|
|
23
|
+
}
|
|
24
|
+
const t = named(name);
|
|
25
|
+
return current.trace(t.name, t.attributes, f);
|
|
26
|
+
}
|
|
27
|
+
function traceP(name, f) {
|
|
28
|
+
if (!current) {
|
|
29
|
+
return f();
|
|
30
|
+
}
|
|
31
|
+
const t = named(name);
|
|
32
|
+
return current.traceP(t.name, t.attributes, f);
|
|
33
|
+
}
|
|
34
|
+
function traceF(name, f) {
|
|
35
|
+
if (!current) {
|
|
36
|
+
return f;
|
|
37
|
+
}
|
|
38
|
+
const t = named(name);
|
|
39
|
+
return current.traceF(t.name, t.attributes, f);
|
|
40
|
+
}
|
|
41
|
+
// Record a named, timestamped marker on whatever trace is currently active — for
|
|
42
|
+
// annotating "something happened here" (a cache miss, a retry, a validation
|
|
43
|
+
// failure) at a point in time, without opening a child trace. A no-op when no
|
|
44
|
+
// trace is active (or no tracer is set).
|
|
45
|
+
function event(name, attributes) {
|
|
46
|
+
current?.event(name, attributes);
|
|
47
|
+
}
|
|
48
|
+
// Split a `Name` into the trace name and its attributes.
|
|
49
|
+
function named(name) {
|
|
50
|
+
if (typeof name === "string") {
|
|
51
|
+
return { name, attributes: {} };
|
|
52
|
+
}
|
|
53
|
+
const { name: n, ...attributes } = name;
|
|
54
|
+
return { name: n, attributes };
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { type Trampoline, tailRecursive, end, fix };
|
|
2
|
+
/**
|
|
3
|
+
* A value that represents a suspended recursive computation.
|
|
4
|
+
*
|
|
5
|
+
* Use `tailRecursive` or `fix` to build trampolined functions that won't
|
|
6
|
+
* blow the stack on deeply recursive calls. Call `.run()` to evaluate.
|
|
7
|
+
*/
|
|
8
|
+
type Trampoline<A> = End<A> | Rec<A>;
|
|
9
|
+
declare const end: <A>(v: A) => Trampoline<A>;
|
|
10
|
+
declare class End<A> {
|
|
11
|
+
value: A;
|
|
12
|
+
constructor(v: A);
|
|
13
|
+
run(): A;
|
|
14
|
+
map<B>(f: (v: A) => B): Trampoline<B>;
|
|
15
|
+
}
|
|
16
|
+
declare class Rec<A> {
|
|
17
|
+
fun: () => Trampoline<A>;
|
|
18
|
+
constructor(f: () => Trampoline<A>);
|
|
19
|
+
run(): A;
|
|
20
|
+
map<B>(f: (v: A) => B): Trampoline<B>;
|
|
21
|
+
}
|
|
22
|
+
type Fun<A extends unknown[], B> = (...args: A) => B;
|
|
23
|
+
declare function fix<A extends unknown[], R>(f: Fun<[Fun<A, Trampoline<R>>, (r: R) => Trampoline<R>], Fun<A, Trampoline<R>>>): Fun<A, R>;
|
|
24
|
+
declare function tailRecursive<A extends unknown[], R>(f: Fun<A, Trampoline<R>>): Fun<A, Trampoline<R>>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export { tailRecursive, end, fix };
|
|
2
|
+
function run(tramp) {
|
|
3
|
+
let result = tramp;
|
|
4
|
+
while (result instanceof Rec) {
|
|
5
|
+
result = result.fun();
|
|
6
|
+
}
|
|
7
|
+
return result.value;
|
|
8
|
+
}
|
|
9
|
+
const rec = (f) => new Rec(f);
|
|
10
|
+
const end = (v) => new End(v);
|
|
11
|
+
class End {
|
|
12
|
+
value;
|
|
13
|
+
constructor(v) {
|
|
14
|
+
this.value = v;
|
|
15
|
+
}
|
|
16
|
+
run() {
|
|
17
|
+
return run(this);
|
|
18
|
+
}
|
|
19
|
+
map(f) {
|
|
20
|
+
return end(f(this.value));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class Rec {
|
|
24
|
+
fun;
|
|
25
|
+
constructor(f) {
|
|
26
|
+
this.fun = f;
|
|
27
|
+
}
|
|
28
|
+
run() {
|
|
29
|
+
return run(this);
|
|
30
|
+
}
|
|
31
|
+
map(f) {
|
|
32
|
+
const v = this.fun();
|
|
33
|
+
return rec(() => v.map(f));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function fix(f) {
|
|
37
|
+
let lazy_f = (..._) => {
|
|
38
|
+
throw new Error("recursion error");
|
|
39
|
+
};
|
|
40
|
+
const recurse = (...args) => rec(() => lazy_f(...args));
|
|
41
|
+
lazy_f = f(recurse, end);
|
|
42
|
+
return (...args) => lazy_f(...args).run();
|
|
43
|
+
}
|
|
44
|
+
function tailRecursive(f) {
|
|
45
|
+
return (...args) => rec(() => f(...args));
|
|
46
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type default as BTreeType } from "sorted-btree";
|
|
2
|
+
import { Maybe } from "./maybe";
|
|
3
|
+
declare const stringMap: <T>() => TreeMap<string, T>;
|
|
4
|
+
interface Comparable<T> {
|
|
5
|
+
compare(other: T): number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Shared base for {@link TreeMap} and {@link ImmutableTreeMap}.
|
|
9
|
+
*
|
|
10
|
+
* Holds the BTree state, all read methods, and every bulk transformation.
|
|
11
|
+
* The only thing each subclass supplies is mutation methods and a `wrap`
|
|
12
|
+
* factory that lifts a fresh BTree back into its own kind.
|
|
13
|
+
*/
|
|
14
|
+
declare abstract class TreeMapCore<K, V> {
|
|
15
|
+
private readonly _;
|
|
16
|
+
protected tree: BTreeType<K, V>;
|
|
17
|
+
readonly compare: (l: K, r: K) => number;
|
|
18
|
+
protected constructor(tree: BTreeType<K, V>, compare: (l: K, r: K) => number);
|
|
19
|
+
/** Construct a new instance of the same subclass around a fresh BTree. */
|
|
20
|
+
protected abstract wrap<W>(tree: BTreeType<K, W>): TreeMapCore<K, W>;
|
|
21
|
+
get(k: K): Maybe<V>;
|
|
22
|
+
has(k: K): boolean;
|
|
23
|
+
keys(): IterableIterator<K>;
|
|
24
|
+
values(): IterableIterator<V>;
|
|
25
|
+
entries(): IterableIterator<[K, V]>;
|
|
26
|
+
size(): number;
|
|
27
|
+
clone(): this;
|
|
28
|
+
/** Create a new map with keys from both maps. */
|
|
29
|
+
unionWith(other: TreeMapCore<K, V>, f: (old: V, new_: V) => V): this;
|
|
30
|
+
/**
|
|
31
|
+
* Difference in the set of keys.
|
|
32
|
+
* `A.difference(B)` equals A minus all keys present in B.
|
|
33
|
+
*/
|
|
34
|
+
difference(other: TreeMapCore<K, unknown>): this;
|
|
35
|
+
/** Create a new map from keys common to two other maps. */
|
|
36
|
+
intersectionWith<W, X>(other: TreeMapCore<K, W>, f: (left: V, right: W) => X): TreeMapCore<K, X>;
|
|
37
|
+
mapWithKeys<W>(f: (k: K, v: V) => W): TreeMapCore<K, W>;
|
|
38
|
+
map<W>(f: (v: V) => W): TreeMapCore<K, W>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A mutable Map type that requires a comparison function.
|
|
42
|
+
*
|
|
43
|
+
* This is just a wrapper around BTree which requires the comparison function.
|
|
44
|
+
*/
|
|
45
|
+
declare class TreeMap<K, V> extends TreeMapCore<K, V> {
|
|
46
|
+
static new<K, V>(compare: (l: K, r: K) => number): TreeMap<K, V>;
|
|
47
|
+
static new_<K extends Comparable<K>, V>(): TreeMap<K, V>;
|
|
48
|
+
static from<K, V>(compare: (l: K, r: K) => number, xs: Array<[K, V]>): TreeMap<K, V>;
|
|
49
|
+
static from_<K extends Comparable<K>, V>(xs: Array<[K, V]>): TreeMap<K, V>;
|
|
50
|
+
protected wrap<W>(tree: BTreeType<K, W>): TreeMap<K, W>;
|
|
51
|
+
set(k: K, v: V): this;
|
|
52
|
+
setWith(k: K, v: V, f: (old: V, _new: V) => V): this;
|
|
53
|
+
remove(k: K): this;
|
|
54
|
+
setEntries(it: IterableIterator<[K, V]>): this;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* An immutable Map type that requires a comparison function.
|
|
58
|
+
*
|
|
59
|
+
* Every update returns a new ImmutableTreeMap; the receiver is untouched.
|
|
60
|
+
* Each update clones the underlying BTree before mutating the copy.
|
|
61
|
+
*/
|
|
62
|
+
declare class ImmutableTreeMap<K, V> extends TreeMapCore<K, V> {
|
|
63
|
+
static new<K, V>(compare: (l: K, r: K) => number): ImmutableTreeMap<K, V>;
|
|
64
|
+
static new_<K extends Comparable<K>, V>(): ImmutableTreeMap<K, V>;
|
|
65
|
+
static from<K, V>(compare: (l: K, r: K) => number, xs: Array<[K, V]>): ImmutableTreeMap<K, V>;
|
|
66
|
+
static from_<K extends Comparable<K>, V>(xs: Array<[K, V]>): ImmutableTreeMap<K, V>;
|
|
67
|
+
protected wrap<W>(tree: BTreeType<K, W>): ImmutableTreeMap<K, W>;
|
|
68
|
+
set(k: K, v: V): ImmutableTreeMap<K, V>;
|
|
69
|
+
setWith(k: K, v: V, f: (old: V, _new: V) => V): ImmutableTreeMap<K, V>;
|
|
70
|
+
remove(k: K): ImmutableTreeMap<K, V>;
|
|
71
|
+
setEntries(it: IterableIterator<[K, V]>): ImmutableTreeMap<K, V>;
|
|
72
|
+
}
|
|
73
|
+
export { TreeMap, ImmutableTreeMap, TreeMapCore, stringMap };
|