@visiolab/effect-firebase-functions 0.0.0-stage → 0.3.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,188 @@
1
+ /**
2
+ * The Document Trigger kind's runtime: serves one Document Trigger as an
3
+ * `onDocument*` export.
4
+ *
5
+ * @internal
6
+ */
7
+ import { Cause, Effect, Exit, type Layer, Option } from "effect";
8
+ import * as AdminFirestoreValue from "@visiolab/effect-firebase-admin/firestore/AdminFirestoreValue";
9
+ import * as Collection from "@visiolab/effect-firebase/firestore/Collection";
10
+ import * as DocumentSnapshot from "@visiolab/effect-firebase/firestore/DocumentSnapshot";
11
+ import * as DocumentTrigger from "@visiolab/effect-firebase/functions/DocumentTrigger";
12
+ import type * as FunctionGroup from "@visiolab/effect-firebase/functions/FunctionGroup";
13
+ import * as Firestore from "firebase-functions/v2/firestore";
14
+ import * as Member from "./member.ts";
15
+ import * as Runtime from "./runtime.ts";
16
+
17
+ /**
18
+ * The typed reference of the document an event is about, from its path
19
+ * relative to the database's `documents/`. A path outside the trigger's
20
+ * collection can't come from its own path pattern, so it is a defect.
21
+ */
22
+ const refOf = (trigger: DocumentTrigger.Any, path: string) =>
23
+ Option.match(Collection.fromPath(trigger.collection, path), {
24
+ onNone: () =>
25
+ Effect.die(
26
+ new Error(
27
+ `The document "${path}" is not in the collection of the trigger "${trigger.name}"`,
28
+ ),
29
+ ),
30
+ onSome: Effect.succeed,
31
+ });
32
+
33
+ /**
34
+ * An existing Document Snapshot at `ref` of the Admin SDK's snapshot, its
35
+ * data read into Firestore Values as a read does. Data that isn't a Firestore
36
+ * Value, such as a vector, is a defect.
37
+ */
38
+ const existingOf = (
39
+ ref: Collection.DocumentReference<Collection.Any>,
40
+ snapshot: Firestore.DocumentSnapshot | undefined,
41
+ ): Effect.Effect<DocumentSnapshot.Existing<Collection.Any>> =>
42
+ snapshot === undefined
43
+ ? Effect.die(new Error(`The event of "${ref.path}" carries no document`))
44
+ : Effect.map(
45
+ Effect.orDie(Effect.fromResult(AdminFirestoreValue.fromAdmin(snapshot.data()))),
46
+ (raw) => DocumentSnapshot.existing(ref, raw),
47
+ );
48
+
49
+ /**
50
+ * The document of an event that hands over one, as an existing Document
51
+ * Snapshot: the created one, or the deleted one as it was just before.
52
+ */
53
+ const snapshotOf = (
54
+ ref: Collection.DocumentReference<Collection.Any>,
55
+ snapshot: Firestore.QueryDocumentSnapshot | undefined,
56
+ ) => Effect.map(existingOf(ref, snapshot), (snapshot) => ({ snapshot }));
57
+
58
+ /**
59
+ * A Document Snapshot at `ref` of a side of a write: `Missing` for the side
60
+ * that doesn't exist, as the Admin SDK marks it with `exists`.
61
+ */
62
+ const sideOf = (
63
+ ref: Collection.DocumentReference<Collection.Any>,
64
+ snapshot: Firestore.DocumentSnapshot,
65
+ ): Effect.Effect<DocumentSnapshot.DocumentSnapshot<Collection.Any>> =>
66
+ snapshot.exists ? existingOf(ref, snapshot) : Effect.succeed(DocumentSnapshot.missing(ref));
67
+
68
+ /**
69
+ * Both sides of a change, each read with `side`: both existing for an update,
70
+ * and either one missing for a write. An event without its change is a defect.
71
+ */
72
+ const changeOf =
73
+ <S, A>(
74
+ side: (ref: Collection.DocumentReference<Collection.Any>, snapshot: S) => Effect.Effect<A>,
75
+ ) =>
76
+ (ref: Collection.DocumentReference<Collection.Any>, change: Firestore.Change<S> | undefined) =>
77
+ change === undefined
78
+ ? Effect.die(new Error(`The event of "${ref.path}" carries no change`))
79
+ : Effect.all({ before: side(ref, change.before), after: side(ref, change.after) });
80
+
81
+ /**
82
+ * The conversion of what an `onDocument*` provider hands over into the core
83
+ * event of a trigger: `read` reads the event's data at the document's typed
84
+ * reference, and `make` builds the core event from that and the CloudEvent's
85
+ * `id` and `time`.
86
+ */
87
+ const conversion =
88
+ <Data, Fields>(
89
+ read: (ref: Collection.DocumentReference<Collection.Any>, data: Data) => Effect.Effect<Fields>,
90
+ make: (input: Fields & { readonly id: string; readonly time: string }) => unknown,
91
+ ) =>
92
+ (trigger: DocumentTrigger.Any, event: Firestore.FirestoreEvent<Data>) =>
93
+ Effect.flatMap(refOf(trigger, event.document), (ref) =>
94
+ Effect.map(read(ref, event.data), (fields) =>
95
+ make({ ...fields, id: event.id, time: event.time }),
96
+ ),
97
+ );
98
+
99
+ const createdEvent = conversion(snapshotOf, DocumentTrigger.createdEvent);
100
+ const deletedEvent = conversion(snapshotOf, DocumentTrigger.deletedEvent);
101
+ const updatedEvent = conversion(changeOf(existingOf), DocumentTrigger.updatedEvent);
102
+ const writtenEvent = conversion(changeOf(sideOf), DocumentTrigger.writtenEvent);
103
+
104
+ /**
105
+ * Logs the Cause of a failed event or Layer build in the event's span.
106
+ */
107
+ const logFailure = (message: string) => (cause: Cause.Cause<unknown>) =>
108
+ Effect.logError(message, cause);
109
+
110
+ /**
111
+ * `DocumentTrigger.serve` of one trigger, with its requirements erased, since
112
+ * the public signatures check that `live` meets them.
113
+ */
114
+ type Serve = Effect.Effect<(event: unknown) => Effect.Effect<void, unknown>, never, unknown>;
115
+
116
+ /**
117
+ * What an `onDocument*` provider returns, by the change it runs on.
118
+ *
119
+ * @internal
120
+ */
121
+ export type Export =
122
+ | ReturnType<typeof Firestore.onDocumentCreated>
123
+ | ReturnType<typeof Firestore.onDocumentUpdated>
124
+ | ReturnType<typeof Firestore.onDocumentWritten>;
125
+
126
+ /**
127
+ * The export of `trigger` in `group`, or in no group: the `onDocument*`
128
+ * export of its change (`onDocumentCreated`, `onDocumentDeleted`,
129
+ * `onDocumentUpdated` or `onDocumentWritten`) on the `(default)` database, at
130
+ * the path pattern of the trigger's collection, that converts each event and
131
+ * runs it through `DocumentTrigger.serve`. It builds `live` on its own first
132
+ * event. A failed event, or a failed build, is logged and thrown, so Firebase
133
+ * delivers it again when `retry` is set.
134
+ *
135
+ * @internal
136
+ */
137
+ export const deploy = <R, E>(
138
+ group: FunctionGroup.Any | undefined,
139
+ trigger: DocumentTrigger.Any,
140
+ live: Layer.Layer<R, E>,
141
+ options: object = {},
142
+ ): Export => {
143
+ const deployment = Member.call(group, trigger, DocumentTrigger.deployment);
144
+ const serve = Member.call<Serve>(group, trigger, DocumentTrigger.serve);
145
+ const run = Runtime.make(live, {
146
+ span: `DocumentTrigger.${deployment.name}`,
147
+ attributes: { "faas.name": deployment.name, "faas.trigger": "datasource" },
148
+ onBuildFailure: logFailure("The Layer of a Document Trigger failed to build"),
149
+ });
150
+ /** The raw handler that converts each event with `convert` and serves it. */
151
+ const handler =
152
+ <Data>(
153
+ convert: (
154
+ trigger: DocumentTrigger.Any,
155
+ event: Firestore.FirestoreEvent<Data>,
156
+ ) => Effect.Effect<unknown>,
157
+ ) =>
158
+ async (event: Firestore.FirestoreEvent<Data>) => {
159
+ const exit = await run(
160
+ serve.pipe(
161
+ Effect.flatMap((handle) =>
162
+ Effect.flatMap(convert(trigger, event), (input) => handle(input)),
163
+ ),
164
+ // Logged with the services of `live`, so a logger it provides is used.
165
+ Effect.onError(logFailure("A Document Trigger failed")),
166
+ ),
167
+ );
168
+ if (Exit.isFailure(exit)) throw Cause.squash(exit.cause);
169
+ };
170
+ // After `options`, so a caller that passes event filters past the public
171
+ // types can't move the trigger to another path, database or namespace.
172
+ const providerOptions = {
173
+ ...options,
174
+ document: deployment.document,
175
+ database: "(default)",
176
+ namespace: "(default)",
177
+ };
178
+ switch (trigger.event) {
179
+ case "created":
180
+ return Firestore.onDocumentCreated(providerOptions, handler(createdEvent));
181
+ case "deleted":
182
+ return Firestore.onDocumentDeleted(providerOptions, handler(deletedEvent));
183
+ case "updated":
184
+ return Firestore.onDocumentUpdated(providerOptions, handler(updatedEvent));
185
+ case "written":
186
+ return Firestore.onDocumentWritten(providerOptions, handler(writtenEvent));
187
+ }
188
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Where `Functions.make` and `Functions.group` dispatch on a Function's kind.
3
+ * A new kind adds its runtime's `deploy` here, and its types to the
4
+ * kind-dispatched types of `Functions.ts` (`Export`, `Options`, `Services`).
5
+ *
6
+ * @internal
7
+ */
8
+ import type { Layer } from "effect";
9
+ import type * as Callable from "@visiolab/effect-firebase/functions/Callable";
10
+ import type * as DocumentTrigger from "@visiolab/effect-firebase/functions/DocumentTrigger";
11
+ import type * as FirebaseFunction from "@visiolab/effect-firebase/functions/FirebaseFunction";
12
+ import type * as FunctionGroup from "@visiolab/effect-firebase/functions/FunctionGroup";
13
+ import * as callable from "./callable.ts";
14
+ import * as documentTrigger from "./documentTrigger.ts";
15
+
16
+ /**
17
+ * The export of `fn` in `group`, or in no group, from its kind's runtime.
18
+ * `options` are the member's own, typed by kind in the public signatures.
19
+ *
20
+ * @internal
21
+ */
22
+ export const deploy = <R, E>(
23
+ group: FunctionGroup.Any | undefined,
24
+ fn: FirebaseFunction.Any,
25
+ live: Layer.Layer<R, E>,
26
+ options: object | undefined,
27
+ ): unknown => {
28
+ switch (fn.kind) {
29
+ case "Callable":
30
+ return callable.deploy(group, fn as Callable.Any, live, options);
31
+ case "DocumentTrigger":
32
+ return documentTrigger.deploy(group, fn as DocumentTrigger.Any, live, options);
33
+ default:
34
+ throw new Error(`Functions can't deploy "${fn.name}": its kind "${fn.kind}" is unknown`);
35
+ }
36
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * How a kind's runtime calls the core functions that take a Function alone or
3
+ * a Function Group and a member's name, such as `deployment` and `serve`.
4
+ *
5
+ * @internal
6
+ */
7
+ import type * as FirebaseFunction from "@visiolab/effect-firebase/functions/FirebaseFunction";
8
+ import type * as FunctionGroup from "@visiolab/effect-firebase/functions/FunctionGroup";
9
+
10
+ /**
11
+ * Calls `api` with `fn` alone, or with `group` and the name of `fn` when it is
12
+ * a member of `group`. The kind's public signatures already checked that `fn`
13
+ * is of the kind `api` takes, so its arguments are erased.
14
+ *
15
+ * @internal
16
+ */
17
+ export const call = <A>(
18
+ group: FunctionGroup.Any | undefined,
19
+ fn: FirebaseFunction.Any,
20
+ api: (...args: ReadonlyArray<never>) => A,
21
+ ): A => (group === undefined ? api(fn as never) : api(group as never, fn.name as never));
@@ -0,0 +1,110 @@
1
+ /**
2
+ * What every kind of Function runs an event or a call under: the lazy,
3
+ * memoized build of its Layer, its server span, its Tracer and its logs.
4
+ *
5
+ * A kind's runtime turns what `firebase-functions` hands over into the input
6
+ * of its core `serve`, and passes that to {@link make}'s run function, which
7
+ * does the rest.
8
+ *
9
+ * @internal
10
+ */
11
+ import { type Cause, Context, Effect, Exit, Layer, Logger, Scope } from "effect";
12
+ import * as CloudLogging from "../logger/CloudLogging.ts";
13
+ import { DeferredTracer, externalParent } from "./serverSpan.ts";
14
+
15
+ /**
16
+ * The memo map and scope every Layer of this process is built in, so a Layer
17
+ * that several exports share is built once. The scope is never closed:
18
+ * functions-framework exits the process without waiting for finalizers.
19
+ */
20
+ const memoMap = Layer.makeMemoMapUnsafe();
21
+ const scope = Scope.makeUnsafe();
22
+
23
+ /**
24
+ * Builds `live` on the first event, and hands every later event the same
25
+ * services. Concurrent first events share one build. The build runs under the
26
+ * span and Tracer of the event that starts it, so it nests under that event.
27
+ *
28
+ * A failed build fails the event and is tried again on the next one. A Layer
29
+ * that fails closes the scope it was built in, which drops its failed entries
30
+ * from the memo map, so the retry builds anew.
31
+ */
32
+ const lazyContext = <R, E>(live: Layer.Layer<R, E>) => {
33
+ let pending: Promise<Exit.Exit<Context.Context<R>, E>> | undefined;
34
+ return Effect.flatMap(Effect.context<never>(), (caller) => {
35
+ pending ??= Effect.runPromiseExit(
36
+ Layer.buildWithMemoMap(live, memoMap, scope).pipe(Effect.provideContext(caller)),
37
+ ).then((exit) => {
38
+ if (Exit.isFailure(exit)) pending = undefined;
39
+ return exit;
40
+ });
41
+ const attempt = pending;
42
+ return Effect.flatten(Effect.promise(() => attempt));
43
+ });
44
+ };
45
+
46
+ /**
47
+ * How one export runs each event or call.
48
+ *
49
+ * @internal
50
+ */
51
+ export interface Options {
52
+ /** The name of the `kind: "server"` span each event or call runs in. */
53
+ readonly span: string;
54
+ /** The attributes of that span. */
55
+ readonly attributes: Readonly<Record<string, unknown>>;
56
+ /**
57
+ * Logs the Cause of a Layer that failed to build, in the span of the event
58
+ * or call that built it.
59
+ */
60
+ readonly onBuildFailure: (cause: Cause.Cause<unknown>) => Effect.Effect<void>;
61
+ }
62
+
63
+ /**
64
+ * The run function of one export: it builds `live` on the export's first
65
+ * event or call, so a worker that serves one export builds nothing for the
66
+ * others, and runs each `body` with the services of `live` provided.
67
+ *
68
+ * The run happens in a `kind: "server"` span, parented on the trace context
69
+ * Firebase read from the event or request. Its spans go to the Tracer of
70
+ * `live`, its logs go through `CloudLogging.layer` unless `live` provides a
71
+ * logger, and logs from before `live` is built wait until the spans have
72
+ * their final ids.
73
+ *
74
+ * Read the trace context synchronously, at the start of the SDK's handler:
75
+ * call the run function there.
76
+ *
77
+ * @internal
78
+ */
79
+ export const make = <R, E>(live: Layer.Layer<R, E>, options: Options) => {
80
+ const context = lazyContext(live);
81
+ const spanOptions = { kind: "server", attributes: options.attributes } as const;
82
+ return <A>(
83
+ body: Effect.Effect<A, unknown, unknown>,
84
+ signal?: AbortSignal,
85
+ ): Promise<Exit.Exit<A, unknown>> => {
86
+ const parent = externalParent();
87
+ const deferred = new DeferredTracer();
88
+ return Effect.runPromiseExit(
89
+ Effect.gen(function* () {
90
+ const services = yield* context.pipe(
91
+ // With no services to take a Tracer from, the run's spans and held
92
+ // logs go to the native Tracer, so the failure is logged in one trace.
93
+ Effect.onError(() => Effect.sync(() => deferred.attach(Context.empty()))),
94
+ Effect.onError(options.onBuildFailure),
95
+ );
96
+ deferred.attach(services as Context.Context<never>);
97
+ // The public signatures check that `live` provides what `body` needs.
98
+ return yield* Effect.provideContext(body, services as Context.Context<any>);
99
+ }).pipe(
100
+ Effect.withSpan(options.span, { ...spanOptions, parent }),
101
+ // Logs from before `live` is built wait for the spans' real ids.
102
+ Effect.updateService(Logger.CurrentLoggers, (loggers) => deferred.holdLogs(loggers)),
103
+ // Beneath `live`: a logger that `live` provides replaces this one.
104
+ Effect.provide(CloudLogging.layer),
105
+ Effect.withTracer(deferred.tracer),
106
+ ),
107
+ signal === undefined ? undefined : { signal },
108
+ );
109
+ };
110
+ };
@@ -0,0 +1,203 @@
1
+ /** @internal */
2
+ import { Context, Logger, Option, Tracer, type Exit, type Fiber } from "effect";
3
+ import { traceContext } from "firebase-functions/v2";
4
+
5
+ /**
6
+ * The parent of a call's server span: the trace context `firebase-functions`
7
+ * read from the request's headers, the `X-Cloud-Trace-Context` header winning
8
+ * over `traceparent`. Read it synchronously at the start of the call, before
9
+ * any fiber runs: Effect's scheduler may later run under another request's
10
+ * async store. `undefined` when the request carried none, as in the emulator.
11
+ *
12
+ * @internal
13
+ */
14
+ export const externalParent = (): Tracer.ExternalSpan | undefined => {
15
+ const store = traceContext.getStore();
16
+ if (store === undefined) return undefined;
17
+ return Tracer.externalSpan({
18
+ traceId: store.traceId,
19
+ // The Cloud header's decimal span id reaches us as unpadded hex.
20
+ spanId: store.parentId.padStart(16, "0"),
21
+ sampled: store.sample,
22
+ });
23
+ };
24
+
25
+ type SpanOptions = Parameters<Tracer.Tracer["span"]>[0];
26
+
27
+ /**
28
+ * A span opened before the Tracer of `Live` is known. It records what happens
29
+ * to it, and replays that onto the real span once {@link DeferredTracer.attach}
30
+ * creates it. From then on it forwards everything.
31
+ */
32
+ class DeferredSpan implements Tracer.Span {
33
+ readonly _tag = "Span";
34
+ readonly name: string;
35
+ readonly parent: Option.Option<Tracer.AnySpan>;
36
+ readonly annotations: Context.Context<never>;
37
+ readonly kind: Tracer.SpanKind;
38
+ readonly sampled: boolean;
39
+ readonly options: SpanOptions;
40
+ real: Tracer.Span | undefined;
41
+ // What happened before `real` existed, in order.
42
+ private readonly calls: Array<(span: Tracer.Span) => void> = [];
43
+ private readonly pendingAttributes = new Map<string, unknown>();
44
+ private readonly pendingLinks: Array<Tracer.SpanLink>;
45
+ private pendingStatus: Tracer.SpanStatus;
46
+ private provisional: { traceId: string; spanId: string } | undefined;
47
+
48
+ constructor(options: SpanOptions) {
49
+ this.options = options;
50
+ this.name = options.name;
51
+ this.parent = options.parent;
52
+ this.annotations = options.annotations;
53
+ this.kind = options.kind;
54
+ this.sampled = options.sampled;
55
+ this.pendingLinks = [...options.links];
56
+ this.pendingStatus = { _tag: "Started", startTime: options.startTime };
57
+ }
58
+
59
+ // Read before `attach` only by a Tracer that `Live` provides to its own
60
+ // Layers: such a span joins the trace, but its parent's span id is one no
61
+ // exporter sees.
62
+ private get ids() {
63
+ if (this.provisional === undefined) {
64
+ const native = new Tracer.NativeSpan({ ...this.options, links: [] });
65
+ this.provisional = { traceId: native.traceId, spanId: native.spanId };
66
+ }
67
+ return this.provisional;
68
+ }
69
+
70
+ get traceId() {
71
+ return this.real?.traceId ?? this.ids.traceId;
72
+ }
73
+
74
+ get spanId() {
75
+ return this.real?.spanId ?? this.ids.spanId;
76
+ }
77
+
78
+ get status() {
79
+ return this.real?.status ?? this.pendingStatus;
80
+ }
81
+
82
+ get attributes() {
83
+ return this.real?.attributes ?? this.pendingAttributes;
84
+ }
85
+
86
+ get links() {
87
+ return this.real?.links ?? this.pendingLinks;
88
+ }
89
+
90
+ end(endTime: bigint, exit: Exit.Exit<unknown, unknown>) {
91
+ if (this.real !== undefined) return this.real.end(endTime, exit);
92
+ if (this.pendingStatus._tag === "Ended") return;
93
+ this.pendingStatus = { _tag: "Ended", startTime: this.pendingStatus.startTime, endTime, exit };
94
+ }
95
+
96
+ attribute(key: string, value: unknown) {
97
+ if (this.real !== undefined) return this.real.attribute(key, value);
98
+ this.pendingAttributes.set(key, value);
99
+ }
100
+
101
+ event(name: string, startTime: bigint, attributes?: Record<string, unknown>) {
102
+ if (this.real !== undefined) return this.real.event(name, startTime, attributes);
103
+ this.calls.push((span) => span.event(name, startTime, attributes));
104
+ }
105
+
106
+ addLinks(links: ReadonlyArray<Tracer.SpanLink>) {
107
+ if (this.real !== undefined) return this.real.addLinks(links);
108
+ this.calls.push((span) => span.addLinks(links));
109
+ }
110
+
111
+ /** Opens the real span on `tracer` and replays onto it what happened so far. */
112
+ attach(tracer: Tracer.Tracer) {
113
+ const parent = Option.map(this.parent, (parent) =>
114
+ parent instanceof DeferredSpan ? (parent.real ?? parent) : parent,
115
+ );
116
+ const real = tracer.span({ ...this.options, parent, links: this.pendingLinks });
117
+ for (const [key, value] of this.pendingAttributes) real.attribute(key, value);
118
+ for (const call of this.calls) call(real);
119
+ if (this.pendingStatus._tag === "Ended") {
120
+ real.end(this.pendingStatus.endTime, this.pendingStatus.exit);
121
+ }
122
+ this.real = real;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * The Tracer a call starts under. A call's server span opens before `Live` is
128
+ * built, and so before its Tracer is known, so that a first call's Layer
129
+ * build nests under it. This Tracer defers the spans opened until then, and
130
+ * {@link DeferredTracer.attach} hands them to the Tracer of `Live`, which
131
+ * exports them with their original times and parents.
132
+ *
133
+ * @internal
134
+ */
135
+ export class DeferredTracer {
136
+ private readonly deferred: Array<DeferredSpan> = [];
137
+ private readonly held: Array<() => void> = [];
138
+ private real: Tracer.Tracer | undefined;
139
+
140
+ /**
141
+ * Wraps each of `loggers` so that an entry logged before {@link attach}
142
+ * is held, and logged on `attach`, once the spans it was logged under have
143
+ * their real ids. A logger reads the trace of the current span, and a
144
+ * deferred span's ids before `attach` are ones no exporter sees.
145
+ *
146
+ * The tracer logger isn't held: it writes onto the span itself, which a
147
+ * deferred span already replays.
148
+ */
149
+ holdLogs(
150
+ loggers: ReadonlySet<Logger.Logger<unknown, any>>,
151
+ ): ReadonlySet<Logger.Logger<unknown, any>> {
152
+ return new Set(
153
+ Array.from(loggers, (logger) =>
154
+ logger === Logger.tracerLogger
155
+ ? logger
156
+ : Logger.make<unknown, unknown>((options) => {
157
+ if (this.real !== undefined) return logger.log(options);
158
+ const held = { ...options, fiber: snapshot(options.fiber) };
159
+ this.held.push(() => logger.log(held));
160
+ }),
161
+ ),
162
+ );
163
+ }
164
+
165
+ readonly tracer: Tracer.Tracer = Tracer.make({
166
+ span: (options) => {
167
+ if (this.real !== undefined) return this.real.span(options);
168
+ const span = new DeferredSpan(options);
169
+ this.deferred.push(span);
170
+ return span;
171
+ },
172
+ });
173
+
174
+ /**
175
+ * Hands every span opened so far to the Tracer that `services` provide, the
176
+ * native one when they provide none, in the order they were opened, so that
177
+ * each parent exists before its children. Then logs the entries
178
+ * {@link holdLogs} held, in the order they were logged.
179
+ *
180
+ * Attach with no services when `Live` failed to build, so the spans and
181
+ * logs of the failed call still get ids of one trace.
182
+ */
183
+ attach(services: Context.Context<never>) {
184
+ if (this.real !== undefined) return;
185
+ this.real = Context.get(services, Tracer.Tracer);
186
+ for (const span of this.deferred) span.attach(this.real);
187
+ this.deferred.length = 0;
188
+ for (const log of this.held) log();
189
+ this.held.length = 0;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * `fiber` as it is now: what a logger reads from it later, such as the log
195
+ * annotations and the current span, stays what it was when the entry was
196
+ * logged. Both are derived from the fiber's context, which never changes in
197
+ * place, only gets replaced.
198
+ */
199
+ const snapshot = <A, E>(fiber: Fiber.Fiber<A, E>): Fiber.Fiber<A, E> =>
200
+ Object.create(fiber, {
201
+ context: { value: fiber.context },
202
+ cache: { value: fiber.cache },
203
+ }) as Fiber.Fiber<A, E>;