@ohos-ports/rivetkit-effect 2.3.17-beta.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/dist/Action.d.ts +104 -0
- package/dist/Action.d.ts.map +1 -0
- package/dist/Action.js +50 -0
- package/dist/Action.js.map +1 -0
- package/dist/Actor.d.ts +132 -0
- package/dist/Actor.d.ts.map +1 -0
- package/dist/Actor.js +104 -0
- package/dist/Actor.js.map +1 -0
- package/dist/Client.d.ts +31 -0
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +100 -0
- package/dist/Client.js.map +1 -0
- package/dist/Registry.d.ts +134 -0
- package/dist/Registry.d.ts.map +1 -0
- package/dist/Registry.js +178 -0
- package/dist/Registry.js.map +1 -0
- package/dist/RivetError.d.ts +480 -0
- package/dist/RivetError.d.ts.map +1 -0
- package/dist/RivetError.js +985 -0
- package/dist/RivetError.js.map +1 -0
- package/dist/RivetLogger.d.ts +41 -0
- package/dist/RivetLogger.d.ts.map +1 -0
- package/dist/RivetLogger.js +41 -0
- package/dist/RivetLogger.js.map +1 -0
- package/dist/State.d.ts +159 -0
- package/dist/State.d.ts.map +1 -0
- package/dist/State.js +98 -0
- package/dist/State.js.map +1 -0
- package/dist/internal/ActionDispatcher.d.ts +14 -0
- package/dist/internal/ActionDispatcher.d.ts.map +1 -0
- package/dist/internal/ActionDispatcher.js +100 -0
- package/dist/internal/ActionDispatcher.js.map +1 -0
- package/dist/internal/ActionErrorEnvelope.d.ts +11 -0
- package/dist/internal/ActionErrorEnvelope.d.ts.map +1 -0
- package/dist/internal/ActionErrorEnvelope.js +14 -0
- package/dist/internal/ActionErrorEnvelope.js.map +1 -0
- package/dist/internal/ActorInstanceManager.d.ts +28 -0
- package/dist/internal/ActorInstanceManager.d.ts.map +1 -0
- package/dist/internal/ActorInstanceManager.js +51 -0
- package/dist/internal/ActorInstanceManager.js.map +1 -0
- package/dist/internal/ActorStateAdapter.d.ts +18 -0
- package/dist/internal/ActorStateAdapter.d.ts.map +1 -0
- package/dist/internal/ActorStateAdapter.js +24 -0
- package/dist/internal/ActorStateAdapter.js.map +1 -0
- package/dist/internal/StateOptions.d.ts +13 -0
- package/dist/internal/StateOptions.d.ts.map +1 -0
- package/dist/internal/StateOptions.js +2 -0
- package/dist/internal/StateOptions.js.map +1 -0
- package/dist/internal/logging.d.ts +22 -0
- package/dist/internal/logging.d.ts.map +1 -0
- package/dist/internal/logging.js +138 -0
- package/dist/internal/logging.js.map +1 -0
- package/dist/internal/tracing.d.ts +23 -0
- package/dist/internal/tracing.d.ts.map +1 -0
- package/dist/internal/tracing.js +30 -0
- package/dist/internal/tracing.js.map +1 -0
- package/dist/internal/utils.d.ts +7 -0
- package/dist/internal/utils.d.ts.map +1 -0
- package/dist/internal/utils.js +7 -0
- package/dist/internal/utils.js.map +1 -0
- package/dist/mod.d.ts +8 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +8 -0
- package/dist/mod.js.map +1 -0
- package/package.json +56 -0
- package/src/Action.ts +231 -0
- package/src/Actor.test-d.ts +641 -0
- package/src/Actor.test.ts +209 -0
- package/src/Actor.ts +575 -0
- package/src/Client.test.ts +206 -0
- package/src/Client.ts +218 -0
- package/src/Registry.test-d.ts +126 -0
- package/src/Registry.test.ts +400 -0
- package/src/Registry.ts +304 -0
- package/src/RivetError.test.ts +199 -0
- package/src/RivetError.ts +1163 -0
- package/src/RivetLogger.ts +60 -0
- package/src/State.test.ts +340 -0
- package/src/State.ts +420 -0
- package/src/internal/ActionDispatcher.ts +189 -0
- package/src/internal/ActionErrorEnvelope.ts +19 -0
- package/src/internal/ActorInstanceManager.ts +137 -0
- package/src/internal/ActorStateAdapter.ts +82 -0
- package/src/internal/StateOptions.ts +21 -0
- package/src/internal/logging.test.ts +274 -0
- package/src/internal/logging.ts +197 -0
- package/src/internal/tracing.ts +42 -0
- package/src/internal/utils.ts +12 -0
- package/src/mod.ts +7 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Context, Effect, Exit, type Fiber, FiberSet, Scope } from "effect";
|
|
2
|
+
import type * as Rivetkit from "@ohos-ports/rivetkit";
|
|
3
|
+
import type * as RivetkitDb from "@ohos-ports/rivetkit/db";
|
|
4
|
+
import type * as ActorStateAdapter from "./ActorStateAdapter.ts";
|
|
5
|
+
import type * as StateOptions from "./StateOptions.ts";
|
|
6
|
+
|
|
7
|
+
type RivetkitDefinitionFor<
|
|
8
|
+
StateDefinition extends StateOptions.Any,
|
|
9
|
+
Database extends RivetkitDb.AnyDatabaseProvider,
|
|
10
|
+
> = Rivetkit.ActorDefinition<
|
|
11
|
+
StateOptions.Encoded<StateDefinition>,
|
|
12
|
+
undefined,
|
|
13
|
+
undefined,
|
|
14
|
+
undefined,
|
|
15
|
+
undefined,
|
|
16
|
+
Database,
|
|
17
|
+
Record<never, never>,
|
|
18
|
+
Record<never, never>,
|
|
19
|
+
any
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
type WakeContext<
|
|
23
|
+
StateDefinition extends StateOptions.Any,
|
|
24
|
+
Database extends RivetkitDb.AnyDatabaseProvider,
|
|
25
|
+
> = Rivetkit.WakeContextOf<RivetkitDefinitionFor<StateDefinition, Database>>;
|
|
26
|
+
|
|
27
|
+
export type Instance<
|
|
28
|
+
ActionHandlers,
|
|
29
|
+
StateDefinition extends StateOptions.Any,
|
|
30
|
+
> = {
|
|
31
|
+
readonly actionHandlers: ActionHandlers;
|
|
32
|
+
readonly runFork: <A, E>(
|
|
33
|
+
effect: Effect.Effect<A, E, any>,
|
|
34
|
+
options?: Effect.RunOptions,
|
|
35
|
+
) => Fiber.Fiber<A, E>;
|
|
36
|
+
readonly scope: Scope.Closeable;
|
|
37
|
+
readonly state?: ActorStateAdapter.ActorState<StateDefinition>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const make = Effect.fnUntraced(function* <
|
|
41
|
+
ActionHandlers,
|
|
42
|
+
StateDefinition extends StateOptions.Any,
|
|
43
|
+
Database extends RivetkitDb.AnyDatabaseProvider,
|
|
44
|
+
WakeOptions,
|
|
45
|
+
>({
|
|
46
|
+
wakeHandler,
|
|
47
|
+
stateAdapter,
|
|
48
|
+
makeContext,
|
|
49
|
+
makeWakeOptions,
|
|
50
|
+
}: {
|
|
51
|
+
readonly wakeHandler: (
|
|
52
|
+
wakeOptions: WakeOptions,
|
|
53
|
+
) => Effect.Effect<ActionHandlers, never, any>;
|
|
54
|
+
readonly stateAdapter:
|
|
55
|
+
| ActorStateAdapter.Adapter<StateDefinition>
|
|
56
|
+
| undefined;
|
|
57
|
+
readonly makeContext: (
|
|
58
|
+
c: WakeContext<StateDefinition, Database>,
|
|
59
|
+
scope: Scope.Closeable,
|
|
60
|
+
) => Context.Context<any>;
|
|
61
|
+
readonly makeWakeOptions: (
|
|
62
|
+
c: WakeContext<StateDefinition, Database>,
|
|
63
|
+
state: ActorStateAdapter.ActorState<StateDefinition> | undefined,
|
|
64
|
+
) => WakeOptions;
|
|
65
|
+
}) {
|
|
66
|
+
const instances = new Map<
|
|
67
|
+
string,
|
|
68
|
+
Instance<ActionHandlers, StateDefinition>
|
|
69
|
+
>();
|
|
70
|
+
|
|
71
|
+
const services = yield* Effect.context<any>();
|
|
72
|
+
const runPromise = Effect.runPromiseWith(services);
|
|
73
|
+
|
|
74
|
+
const makeInstance = Effect.fnUntraced(function* (
|
|
75
|
+
c: WakeContext<StateDefinition, Database>,
|
|
76
|
+
): Effect.fn.Return<Instance<ActionHandlers, StateDefinition>, never, any> {
|
|
77
|
+
const scope = yield* Scope.make();
|
|
78
|
+
return yield* Effect.gen(function* () {
|
|
79
|
+
const state = stateAdapter
|
|
80
|
+
? yield* stateAdapter
|
|
81
|
+
.makeStateView(c)
|
|
82
|
+
.pipe(Effect.provideService(Scope.Scope, scope))
|
|
83
|
+
: undefined;
|
|
84
|
+
const context = makeContext(c, scope);
|
|
85
|
+
const actionHandlers = yield* wakeHandler(
|
|
86
|
+
makeWakeOptions(c, state),
|
|
87
|
+
).pipe(Effect.provide(context));
|
|
88
|
+
const runFork = yield* FiberSet.makeRuntime<
|
|
89
|
+
any,
|
|
90
|
+
unknown,
|
|
91
|
+
unknown
|
|
92
|
+
>().pipe(Effect.provide(Context.merge(services, context)));
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
actionHandlers,
|
|
96
|
+
runFork,
|
|
97
|
+
scope,
|
|
98
|
+
state,
|
|
99
|
+
};
|
|
100
|
+
}).pipe(
|
|
101
|
+
Effect.onError((cause) =>
|
|
102
|
+
Scope.close(scope, Exit.failCause(cause)),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
get: (actorId: string) => instances.get(actorId),
|
|
109
|
+
onWake: async (c: WakeContext<StateDefinition, Database>) => {
|
|
110
|
+
instances.set(c.actorId, await runPromise(makeInstance(c)));
|
|
111
|
+
},
|
|
112
|
+
onStateChange: stateAdapter
|
|
113
|
+
? (
|
|
114
|
+
c: WakeContext<StateDefinition, Database>,
|
|
115
|
+
newState: unknown,
|
|
116
|
+
) => {
|
|
117
|
+
const instance = instances.get(c.actorId);
|
|
118
|
+
// State changes can arrive after teardown removes the instance.
|
|
119
|
+
if (!instance) return;
|
|
120
|
+
|
|
121
|
+
stateAdapter.publishChange(instance, newState);
|
|
122
|
+
}
|
|
123
|
+
: undefined,
|
|
124
|
+
onTeardown: async (c: { readonly actorId: string }) => {
|
|
125
|
+
return runPromise(
|
|
126
|
+
Effect.gen(function* () {
|
|
127
|
+
const instance = instances.get(c.actorId);
|
|
128
|
+
// Teardown can be reported through multiple lifecycle callbacks.
|
|
129
|
+
if (!instance) return;
|
|
130
|
+
|
|
131
|
+
instances.delete(c.actorId);
|
|
132
|
+
yield* Scope.close(instance.scope, Exit.void);
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Effect, type Fiber, Schema } from "effect";
|
|
2
|
+
import * as State from "../State.ts";
|
|
3
|
+
import type * as StateOptions from "./StateOptions.ts";
|
|
4
|
+
|
|
5
|
+
export type ActorState<StateDefinition extends StateOptions.Any> = State.State<
|
|
6
|
+
StateOptions.Decoded<StateDefinition>,
|
|
7
|
+
Schema.SchemaError,
|
|
8
|
+
StateOptions.Services<StateDefinition>
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
type StateInstance<StateDefinition extends StateOptions.Any> = {
|
|
12
|
+
readonly runFork: <A, E>(
|
|
13
|
+
effect: Effect.Effect<A, E, any>,
|
|
14
|
+
options?: Effect.RunOptions,
|
|
15
|
+
) => Fiber.Fiber<A, E>;
|
|
16
|
+
readonly state?: ActorState<StateDefinition>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type Adapter<StateDefinition extends StateOptions.Any> = {
|
|
20
|
+
readonly makeStateView: (c: {
|
|
21
|
+
state: StateOptions.Encoded<StateDefinition>;
|
|
22
|
+
}) => Effect.Effect<ActorState<StateDefinition>, never, any>;
|
|
23
|
+
readonly createInitialState: () => Promise<
|
|
24
|
+
StateOptions.Encoded<StateDefinition>
|
|
25
|
+
>;
|
|
26
|
+
readonly publishChange: (
|
|
27
|
+
instance: StateInstance<StateDefinition>,
|
|
28
|
+
newState: unknown,
|
|
29
|
+
) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const make = Effect.fnUntraced(function* <
|
|
33
|
+
StateDefinition extends StateOptions.Any,
|
|
34
|
+
>(
|
|
35
|
+
stateOptions: StateDefinition,
|
|
36
|
+
): Effect.fn.Return<Adapter<StateDefinition>, never, any> {
|
|
37
|
+
const services = yield* Effect.context<any>();
|
|
38
|
+
|
|
39
|
+
const stateCodec = {
|
|
40
|
+
decodeUnknown: Schema.decodeUnknownEffect(
|
|
41
|
+
Schema.toCodecJson(stateOptions.schema),
|
|
42
|
+
),
|
|
43
|
+
encode: Schema.encodeEffect(Schema.toCodecJson(stateOptions.schema)),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
makeStateView: (c) =>
|
|
48
|
+
State.make(
|
|
49
|
+
() => stateCodec.decodeUnknown(c.state),
|
|
50
|
+
(next) =>
|
|
51
|
+
stateCodec.encode(next).pipe(
|
|
52
|
+
Effect.tap((encoded) =>
|
|
53
|
+
Effect.sync(() => {
|
|
54
|
+
c.state = encoded;
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
Effect.asVoid,
|
|
58
|
+
),
|
|
59
|
+
).pipe(
|
|
60
|
+
Effect.orDie,
|
|
61
|
+
Effect.map((state) => state as ActorState<StateDefinition>),
|
|
62
|
+
),
|
|
63
|
+
createInitialState: () =>
|
|
64
|
+
Effect.runPromiseWith(services)(
|
|
65
|
+
stateCodec
|
|
66
|
+
.encode(stateOptions.initialValue())
|
|
67
|
+
.pipe(Effect.orDie),
|
|
68
|
+
),
|
|
69
|
+
publishChange: (instance, newState) => {
|
|
70
|
+
instance.runFork(
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
const state = yield* Effect.fromNullishOr(
|
|
73
|
+
instance.state,
|
|
74
|
+
).pipe(Effect.orDie);
|
|
75
|
+
yield* state[State.RuntimeTypeId].publishEffect(
|
|
76
|
+
stateCodec.decodeUnknown(newState).pipe(Effect.orDie),
|
|
77
|
+
);
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export interface StateOptions<in out S extends Schema.Top> {
|
|
4
|
+
readonly schema: S;
|
|
5
|
+
readonly initialValue: () => S["Type"];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Any {
|
|
9
|
+
readonly schema: Schema.Top;
|
|
10
|
+
readonly initialValue: () => unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Services<State extends Any> =
|
|
14
|
+
| State["schema"]["DecodingServices"]
|
|
15
|
+
| State["schema"]["EncodingServices"];
|
|
16
|
+
|
|
17
|
+
export type Encoded<State extends Any> =
|
|
18
|
+
| State["schema"]["Encoded"]
|
|
19
|
+
| ([State] extends [never] ? undefined : never);
|
|
20
|
+
|
|
21
|
+
export type Decoded<State extends Any> = State["schema"]["Type"];
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { assert, describe, it } from "@effect/vitest";
|
|
2
|
+
import { ConfigProvider, Effect, Logger, References } from "effect";
|
|
3
|
+
import * as RivetkitLog from "@ohos-ports/rivetkit/log";
|
|
4
|
+
import * as Logging from "./logging.ts";
|
|
5
|
+
|
|
6
|
+
type LogEntry = {
|
|
7
|
+
readonly level: string;
|
|
8
|
+
readonly fields: Record<string, unknown>;
|
|
9
|
+
readonly msg: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function makeTestLogger(entries: Array<LogEntry>): RivetkitLog.Logger {
|
|
13
|
+
const logger: Record<string, unknown> = {};
|
|
14
|
+
for (const level of ["trace", "debug", "info", "warn", "error", "fatal"]) {
|
|
15
|
+
logger[level] = (
|
|
16
|
+
fields: Record<string, unknown>,
|
|
17
|
+
msg?: string,
|
|
18
|
+
): void => {
|
|
19
|
+
entries.push({ level, fields, msg });
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return logger as unknown as RivetkitLog.Logger;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("internal/logging", () => {
|
|
27
|
+
it("serializes actor keys like the RivetKit actor runtime logger", () => {
|
|
28
|
+
assert.strictEqual(Logging.serializeActorKey([]), "/");
|
|
29
|
+
assert.strictEqual(Logging.serializeActorKey(["room", "1"]), "room/1");
|
|
30
|
+
assert.strictEqual(Logging.serializeActorKey(["room/1"]), "room\\/1");
|
|
31
|
+
assert.strictEqual(Logging.serializeActorKey([""]), "\\0");
|
|
32
|
+
assert.strictEqual(Logging.serializeActorKey(["a\\b"]), "a\\\\b");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("builds actor log annotations with serialized keys", () => {
|
|
36
|
+
assert.deepStrictEqual(
|
|
37
|
+
Logging.makeActorLogAnnotations({
|
|
38
|
+
name: "ChatRoom",
|
|
39
|
+
key: ["room/1"],
|
|
40
|
+
actorId: "actor-1",
|
|
41
|
+
}),
|
|
42
|
+
{
|
|
43
|
+
actor: "ChatRoom",
|
|
44
|
+
key: "room\\/1",
|
|
45
|
+
actorId: "actor-1",
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it.effect("writes Effect logs through the RivetKit base logger", () =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const entries: Array<LogEntry> = [];
|
|
53
|
+
const baseLogger = makeTestLogger(entries);
|
|
54
|
+
|
|
55
|
+
yield* Effect.logInfo("room awake", { roomId: "abc" }).pipe(
|
|
56
|
+
Effect.annotateLogs({
|
|
57
|
+
actor: "ChatRoom",
|
|
58
|
+
key: "room-1",
|
|
59
|
+
actorId: "actor-1",
|
|
60
|
+
}),
|
|
61
|
+
Effect.provide(Logger.layer([Logging.makeLogger(baseLogger)])),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const entry = entries[0];
|
|
65
|
+
assert.ok(entry !== undefined);
|
|
66
|
+
assert.strictEqual(entry.level, "info");
|
|
67
|
+
assert.strictEqual(entry.msg, "room awake");
|
|
68
|
+
assert.deepStrictEqual(entry.fields, {
|
|
69
|
+
roomId: "abc",
|
|
70
|
+
actor: "ChatRoom",
|
|
71
|
+
key: "room-1",
|
|
72
|
+
actorId: "actor-1",
|
|
73
|
+
fiberId: entry.fields.fiberId,
|
|
74
|
+
});
|
|
75
|
+
assert.strictEqual(typeof entry.fields.fiberId, "string");
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
it.effect("preserves Error log messages as structured error fields", () =>
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
const entries: Array<LogEntry> = [];
|
|
82
|
+
const baseLogger = makeTestLogger(entries);
|
|
83
|
+
const error = new Error("room failed to wake");
|
|
84
|
+
|
|
85
|
+
yield* Effect.logError(error).pipe(
|
|
86
|
+
Effect.provide(Logger.layer([Logging.makeLogger(baseLogger)])),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const entry = entries[0];
|
|
90
|
+
assert.ok(entry !== undefined);
|
|
91
|
+
assert.strictEqual(entry.level, "error");
|
|
92
|
+
assert.strictEqual(entry.fields.error, error);
|
|
93
|
+
assert.strictEqual(entry.msg, error.message);
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
it.effect("preserves Error log messages with additional fields", () =>
|
|
98
|
+
Effect.gen(function* () {
|
|
99
|
+
const entries: Array<LogEntry> = [];
|
|
100
|
+
const baseLogger = makeTestLogger(entries);
|
|
101
|
+
const error = new Error("action dispatch failed");
|
|
102
|
+
|
|
103
|
+
yield* Effect.logError(error, {
|
|
104
|
+
actorId: "actor-1",
|
|
105
|
+
action: "SendMessage",
|
|
106
|
+
}).pipe(
|
|
107
|
+
Effect.provide(Logger.layer([Logging.makeLogger(baseLogger)])),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const entry = entries[0];
|
|
111
|
+
assert.ok(entry !== undefined);
|
|
112
|
+
assert.strictEqual(entry.level, "error");
|
|
113
|
+
assert.strictEqual(entry.fields.error, error);
|
|
114
|
+
assert.strictEqual(entry.fields.actorId, "actor-1");
|
|
115
|
+
assert.strictEqual(entry.fields.action, "SendMessage");
|
|
116
|
+
assert.strictEqual(entry.msg, error.message);
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
it.effect("accepts RIVET_LOG_LEVEL values", () =>
|
|
121
|
+
Effect.gen(function* () {
|
|
122
|
+
const baseLogger = yield* Logging.makeDefaultBaseLogger;
|
|
123
|
+
|
|
124
|
+
assert.strictEqual(baseLogger.level, "silent");
|
|
125
|
+
}).pipe(
|
|
126
|
+
Effect.provideService(
|
|
127
|
+
ConfigProvider.ConfigProvider,
|
|
128
|
+
ConfigProvider.fromEnv({
|
|
129
|
+
env: {
|
|
130
|
+
RIVET_LOG_LEVEL: "silent",
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
it.effect("accepts uppercase RIVET_LOG_LEVEL values", () =>
|
|
138
|
+
Effect.gen(function* () {
|
|
139
|
+
const baseLogger = yield* Logging.makeDefaultBaseLogger;
|
|
140
|
+
|
|
141
|
+
assert.strictEqual(baseLogger.level, "debug");
|
|
142
|
+
}).pipe(
|
|
143
|
+
Effect.provideService(
|
|
144
|
+
ConfigProvider.ConfigProvider,
|
|
145
|
+
ConfigProvider.fromEnv({
|
|
146
|
+
env: {
|
|
147
|
+
RIVET_LOG_LEVEL: "DEBUG",
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
it.effect("ignores Effect-only RIVET_LOG_LEVEL values", () =>
|
|
155
|
+
Effect.gen(function* () {
|
|
156
|
+
const baseLogger = yield* Logging.makeDefaultBaseLogger;
|
|
157
|
+
|
|
158
|
+
assert.strictEqual(baseLogger.level, "info");
|
|
159
|
+
}).pipe(
|
|
160
|
+
Effect.provideService(References.MinimumLogLevel, "Info"),
|
|
161
|
+
Effect.provideService(
|
|
162
|
+
ConfigProvider.ConfigProvider,
|
|
163
|
+
ConfigProvider.fromEnv({
|
|
164
|
+
env: {
|
|
165
|
+
RIVET_LOG_LEVEL: "None",
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
it.effect("falls back to References.MinimumLogLevel without env", () =>
|
|
173
|
+
Effect.gen(function* () {
|
|
174
|
+
const baseLogger = yield* Logging.makeDefaultBaseLogger;
|
|
175
|
+
|
|
176
|
+
assert.strictEqual(baseLogger.level, "debug");
|
|
177
|
+
}).pipe(
|
|
178
|
+
Effect.provideService(References.MinimumLogLevel, "Debug"),
|
|
179
|
+
Effect.provideService(
|
|
180
|
+
ConfigProvider.ConfigProvider,
|
|
181
|
+
ConfigProvider.fromEnv({
|
|
182
|
+
env: {},
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
it.effect("RIVET_LOG_LEVEL overrides References.MinimumLogLevel", () =>
|
|
189
|
+
Effect.gen(function* () {
|
|
190
|
+
const baseLogger = yield* Logging.makeDefaultBaseLogger;
|
|
191
|
+
|
|
192
|
+
assert.strictEqual(baseLogger.level, "silent");
|
|
193
|
+
}).pipe(
|
|
194
|
+
Effect.provideService(References.MinimumLogLevel, "Debug"),
|
|
195
|
+
Effect.provideService(
|
|
196
|
+
ConfigProvider.ConfigProvider,
|
|
197
|
+
ConfigProvider.fromEnv({
|
|
198
|
+
env: {
|
|
199
|
+
RIVET_LOG_LEVEL: "silent",
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
it.effect(
|
|
207
|
+
"uses References.CurrentLogLevel for plain Effect.log calls",
|
|
208
|
+
() =>
|
|
209
|
+
Effect.gen(function* () {
|
|
210
|
+
const entries: Array<LogEntry> = [];
|
|
211
|
+
const baseLogger = makeTestLogger(entries);
|
|
212
|
+
|
|
213
|
+
yield* Effect.log("plain log").pipe(
|
|
214
|
+
Effect.provideService(References.CurrentLogLevel, "Debug"),
|
|
215
|
+
Effect.provideService(References.MinimumLogLevel, "Debug"),
|
|
216
|
+
Effect.provide(
|
|
217
|
+
Logger.layer([Logging.makeLogger(baseLogger)]),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const entry = entries[0];
|
|
222
|
+
assert.ok(entry !== undefined);
|
|
223
|
+
assert.strictEqual(entry.level, "debug");
|
|
224
|
+
assert.strictEqual(entry.msg, "plain log");
|
|
225
|
+
assert.deepStrictEqual(entry.fields, {
|
|
226
|
+
fiberId: entry.fields.fiberId,
|
|
227
|
+
});
|
|
228
|
+
assert.strictEqual(typeof entry.fields.fiberId, "string");
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
it.effect(
|
|
233
|
+
"does not call a Pino method for the None current log level",
|
|
234
|
+
() =>
|
|
235
|
+
Effect.gen(function* () {
|
|
236
|
+
const entries: Array<LogEntry> = [];
|
|
237
|
+
const baseLogger = makeTestLogger(entries);
|
|
238
|
+
|
|
239
|
+
yield* Effect.log("hidden log").pipe(
|
|
240
|
+
Effect.provideService(References.CurrentLogLevel, "None"),
|
|
241
|
+
Effect.provideService(References.MinimumLogLevel, "All"),
|
|
242
|
+
Effect.provide(
|
|
243
|
+
Logger.layer([Logging.makeLogger(baseLogger)]),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
assert.deepStrictEqual(entries, []);
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
it.effect(
|
|
252
|
+
"emits References.CurrentLogSpans as structured span durations",
|
|
253
|
+
() =>
|
|
254
|
+
Effect.gen(function* () {
|
|
255
|
+
const entries: Array<LogEntry> = [];
|
|
256
|
+
const baseLogger = makeTestLogger(entries);
|
|
257
|
+
|
|
258
|
+
yield* Effect.logInfo("checkout complete").pipe(
|
|
259
|
+
Effect.withLogSpan("checkout"),
|
|
260
|
+
Effect.provide(
|
|
261
|
+
Logger.layer([Logging.makeLogger(baseLogger)]),
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
assert.strictEqual(entries.length, 1);
|
|
266
|
+
assert.strictEqual(entries[0]?.level, "info");
|
|
267
|
+
assert.strictEqual(entries[0]?.msg, "checkout complete");
|
|
268
|
+
const spans = entries[0]?.fields.spans as
|
|
269
|
+
| Record<string, unknown>
|
|
270
|
+
| undefined;
|
|
271
|
+
assert.strictEqual(typeof spans?.checkout, "number");
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Config,
|
|
3
|
+
Context,
|
|
4
|
+
Effect,
|
|
5
|
+
Logger,
|
|
6
|
+
Option,
|
|
7
|
+
Predicate,
|
|
8
|
+
Record as EffectRecord,
|
|
9
|
+
type LogLevel,
|
|
10
|
+
References,
|
|
11
|
+
} from "effect";
|
|
12
|
+
import type * as Rivetkit from "@ohos-ports/rivetkit";
|
|
13
|
+
import * as RivetkitLog from "@ohos-ports/rivetkit/log";
|
|
14
|
+
|
|
15
|
+
const EMPTY_KEY = "/";
|
|
16
|
+
const KEY_SEPARATOR = "/";
|
|
17
|
+
|
|
18
|
+
type ActorLogContext = {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly key: Rivetkit.ActorKey;
|
|
21
|
+
readonly actorId: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class BaseLogger extends Context.Service<
|
|
25
|
+
BaseLogger,
|
|
26
|
+
RivetkitLog.Logger
|
|
27
|
+
>()("@rivetkit/effect/RivetLogger/BaseLogger") {}
|
|
28
|
+
|
|
29
|
+
const RivetkitLogLevels = RivetkitLog.LogLevelSchema.options;
|
|
30
|
+
|
|
31
|
+
const EffectLevelByRivetkitLevel = {
|
|
32
|
+
trace: "Trace",
|
|
33
|
+
debug: "Debug",
|
|
34
|
+
info: "Info",
|
|
35
|
+
warn: "Warn",
|
|
36
|
+
error: "Error",
|
|
37
|
+
fatal: "Fatal",
|
|
38
|
+
silent: "None",
|
|
39
|
+
} as const satisfies Record<
|
|
40
|
+
RivetkitLog.LogLevel,
|
|
41
|
+
Exclude<LogLevel.LogLevel, "All">
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
const RivetkitLevelByEffectLevel = {
|
|
45
|
+
...Object.fromEntries(
|
|
46
|
+
RivetkitLogLevels.map((level) => [
|
|
47
|
+
EffectLevelByRivetkitLevel[level],
|
|
48
|
+
level,
|
|
49
|
+
]),
|
|
50
|
+
),
|
|
51
|
+
All: "trace",
|
|
52
|
+
} as Record<LogLevel.LogLevel, RivetkitLog.LogLevel>;
|
|
53
|
+
|
|
54
|
+
const rivetLogLevelFromEnv = Config.string("RIVET_LOG_LEVEL").pipe(
|
|
55
|
+
Effect.option,
|
|
56
|
+
Effect.map((maybeRivetLogLevel) => {
|
|
57
|
+
if (Option.isNone(maybeRivetLogLevel)) return Option.none();
|
|
58
|
+
|
|
59
|
+
const parsed = RivetkitLog.LogLevelSchema.safeParse(
|
|
60
|
+
maybeRivetLogLevel.value.toLowerCase(),
|
|
61
|
+
);
|
|
62
|
+
return parsed.success ? Option.some(parsed.data) : Option.none();
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export const makeDefaultBaseLogger: Effect.Effect<RivetkitLog.Logger> =
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
const maybeRivetLogLevel = yield* rivetLogLevelFromEnv;
|
|
69
|
+
const logLevel = Option.isSome(maybeRivetLogLevel)
|
|
70
|
+
? maybeRivetLogLevel.value
|
|
71
|
+
: RivetkitLevelByEffectLevel[yield* References.MinimumLogLevel];
|
|
72
|
+
|
|
73
|
+
return yield* Effect.sync(() =>
|
|
74
|
+
RivetkitLog.makeDefaultLogger(logLevel),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const getOrCreateBaseLogger: Effect.Effect<RivetkitLog.Logger> =
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
const maybeBaseLogger = yield* Effect.serviceOption(BaseLogger);
|
|
81
|
+
if (Option.isSome(maybeBaseLogger)) {
|
|
82
|
+
return maybeBaseLogger.value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return yield* makeDefaultBaseLogger;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export function makeActorLogAnnotations(context: ActorLogContext): {
|
|
89
|
+
readonly actor: string;
|
|
90
|
+
readonly key: string;
|
|
91
|
+
readonly actorId: string;
|
|
92
|
+
} {
|
|
93
|
+
return {
|
|
94
|
+
actor: context.name,
|
|
95
|
+
key: serializeActorKey(context.key),
|
|
96
|
+
actorId: context.actorId,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function serializeActorKey(key: Rivetkit.ActorKey): string {
|
|
101
|
+
if (key.length === 0) {
|
|
102
|
+
return EMPTY_KEY;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return key
|
|
106
|
+
.map((part) => {
|
|
107
|
+
if (part === "") {
|
|
108
|
+
return "\\0";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return part
|
|
112
|
+
.replace(/\\/g, "\\\\")
|
|
113
|
+
.replace(/\//g, `\\${KEY_SEPARATOR}`);
|
|
114
|
+
})
|
|
115
|
+
.join(KEY_SEPARATOR);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function makeLogger(
|
|
119
|
+
baseLogger: RivetkitLog.Logger,
|
|
120
|
+
): Logger.Logger<unknown, void> {
|
|
121
|
+
return Logger.make((options) => {
|
|
122
|
+
if (options.logLevel === "None") return;
|
|
123
|
+
const rivetkitLevel = RivetkitLevelByEffectLevel[options.logLevel];
|
|
124
|
+
const structured = Logger.formatStructured.log(options);
|
|
125
|
+
const { msg, fields: messageFields } = extractMessage(
|
|
126
|
+
structured.message,
|
|
127
|
+
);
|
|
128
|
+
const fields: Record<string, unknown> = {
|
|
129
|
+
...messageFields,
|
|
130
|
+
...structured.annotations,
|
|
131
|
+
fiberId: structured.fiberId,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (!EffectRecord.isEmptyRecord(structured.spans)) {
|
|
135
|
+
fields.spans = structured.spans;
|
|
136
|
+
}
|
|
137
|
+
if (structured.cause !== undefined) {
|
|
138
|
+
fields.cause = structured.cause;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const logger = baseLogger[rivetkitLevel];
|
|
142
|
+
if (msg === undefined) {
|
|
143
|
+
logger.call(baseLogger, fields);
|
|
144
|
+
} else {
|
|
145
|
+
logger.call(baseLogger, fields, msg);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extractMessage(message: unknown): {
|
|
151
|
+
readonly msg: string | undefined;
|
|
152
|
+
readonly fields: Record<string, unknown>;
|
|
153
|
+
} {
|
|
154
|
+
const values = Array.isArray(message) ? message : [message];
|
|
155
|
+
const fields: Record<string, unknown> = {};
|
|
156
|
+
const args: Array<unknown> = [];
|
|
157
|
+
let msg: string | undefined;
|
|
158
|
+
|
|
159
|
+
for (const [index, value] of values.entries()) {
|
|
160
|
+
if (Predicate.isError(value)) {
|
|
161
|
+
fields.error = value;
|
|
162
|
+
if (index === 0) msg = value.message;
|
|
163
|
+
} else if (isStructuredError(value)) {
|
|
164
|
+
fields.error = value;
|
|
165
|
+
if (index === 0) msg = value.error;
|
|
166
|
+
} else if (Predicate.isObject(value)) {
|
|
167
|
+
if (index === 0) {
|
|
168
|
+
const { msg: valueMsg, ...rest } = value;
|
|
169
|
+
Object.assign(fields, rest);
|
|
170
|
+
if (valueMsg !== undefined) msg = String(valueMsg);
|
|
171
|
+
} else {
|
|
172
|
+
Object.assign(fields, value);
|
|
173
|
+
}
|
|
174
|
+
} else if (index === 0) {
|
|
175
|
+
msg = value === undefined ? undefined : String(value);
|
|
176
|
+
} else {
|
|
177
|
+
args.push(value);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (args.length > 0) {
|
|
182
|
+
fields.args = args;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { msg, fields };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isStructuredError(
|
|
189
|
+
value: unknown,
|
|
190
|
+
): value is { readonly error: string; readonly name: string } {
|
|
191
|
+
return (
|
|
192
|
+
Predicate.hasProperty(value, "error") &&
|
|
193
|
+
Predicate.hasProperty(value, "name") &&
|
|
194
|
+
Predicate.isString(value.error) &&
|
|
195
|
+
Predicate.isString(value.name)
|
|
196
|
+
);
|
|
197
|
+
}
|