@danypops/papyrus 0.54.2 → 0.54.3
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/package.json +1 -1
- package/src/daemon/daemon.ts +2 -2
- package/src/log/log.ts +31 -20
- package/src/service.ts +1 -1
- package/src/stores/task-mutation-request-store.ts +16 -1
package/package.json
CHANGED
package/src/daemon/daemon.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { diagnoseDaemon, openDaemonLifecycleLog } from "@danypops/vehicle-server
|
|
|
6
6
|
import { acquireDaemonLock, releaseDaemonLock } from "@danypops/vehicle-server/paths";
|
|
7
7
|
import { PushChannel } from "@danypops/vehicle-server/push-channel";
|
|
8
8
|
import { DAEMON_HOST, DB_OPTIMIZE_INTERVAL_MS, dbPath, WAL_CHECKPOINT_INTERVAL_MS } from "../constants.ts";
|
|
9
|
-
import { logEvent,
|
|
9
|
+
import { logEvent, logger } from "../log/log.ts";
|
|
10
10
|
import { createApp, createPapyrusService } from "../service.ts";
|
|
11
11
|
import {
|
|
12
12
|
clearDaemonPort,
|
|
@@ -78,7 +78,7 @@ export async function serveMain(): Promise<void> {
|
|
|
78
78
|
pushChannel.publish("tasks", { operation });
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
-
logger
|
|
81
|
+
logger,
|
|
82
82
|
diagnose: () => diagnoseDaemon({ lifecycleLog, current: { instanceId, pid: process.pid, startedAt, provenance } }),
|
|
83
83
|
});
|
|
84
84
|
const server = Bun.serve({
|
package/src/log/log.ts
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Structured daemon logging, now backed by `@danypops/vehicle-server/logging` (pino) instead of a
|
|
3
|
+
* hand-rolled `console.error(JSON.stringify(...))` -- gains real level filtering (including a real
|
|
4
|
+
* debug level this daemon previously had no way to emit at all) and a destination injectable for
|
|
5
|
+
* tests, matching jittor/src/log.ts's already-migrated shape exactly. One deliberate, disclosed
|
|
6
|
+
* shape change from the old bespoke format: the event name is now pino's `msg` field rather than a
|
|
7
|
+
* separate `event` field, matching daemon-kit's shared convention across every migrated daemon.
|
|
8
|
+
* `component`/`level`/`timestamp` and credential-safety (callers still must pass only bounded,
|
|
9
|
+
* non-sensitive fields; @danypops/vehicle-server/logging's own default redact list also catches any
|
|
10
|
+
* credential-shaped field that slips in regardless) are unchanged.
|
|
11
|
+
*/
|
|
12
|
+
import { createLogger, type Logger, type LogLevel as VehicleLogLevel } from "@danypops/vehicle-server/logging";
|
|
2
13
|
|
|
3
|
-
export type LogLevel = "info" | "warn" | "error"
|
|
4
|
-
|
|
5
|
-
/** Credential-safe structured daemon event. Callers must pass bounded, non-sensitive fields. */
|
|
6
|
-
export function logEvent(level: LogLevel, event: string, fields: Record<string, unknown> = {}): void {
|
|
7
|
-
console.error(JSON.stringify({ timestamp: new Date().toISOString(), level, component: "papyrus-daemon", event, ...fields }));
|
|
8
|
-
}
|
|
14
|
+
export type LogLevel = Extract<VehicleLogLevel, "info" | "warn" | "error">;
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* Pinned to `console.error` -- rather than `createLogger`'s own default of a raw fd 2 write via
|
|
18
|
+
* `pino.destination(2)`, which bypasses `console.error` entirely -- so existing tooling/tests that
|
|
19
|
+
* intercept `console.error` keep working unchanged (matches jittor/src/log.ts's own reasoning).
|
|
20
|
+
* Also satisfies @danypops/vehicle-server's own `Logger` port directly wherever one is needed (e.g.
|
|
21
|
+
* `createVehicleHttpApp`'s failure logging in daemon.ts), replacing the old `vehicleLogger()`
|
|
22
|
+
* adapter now that this logger already natively implements the full debug/info/warn/error surface.
|
|
17
23
|
*/
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
24
|
+
export const logger: Logger = createLogger("papyrus-daemon", {
|
|
25
|
+
destination: {
|
|
26
|
+
write: (chunk: string) => {
|
|
27
|
+
console.error(chunk.replace(/\n$/, ""));
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Credential-safe structured daemon event. Callers must pass bounded, non-sensitive fields. */
|
|
34
|
+
export function logEvent(level: LogLevel, event: string, fields: Record<string, unknown> = {}): void {
|
|
35
|
+
logger[level](event, fields);
|
|
25
36
|
}
|
package/src/service.ts
CHANGED
|
@@ -594,7 +594,7 @@ export function createApp(deps: {
|
|
|
594
594
|
* (daemon.ts) wires this to a PushChannel; tests and other embedders can ignore it.
|
|
595
595
|
*/
|
|
596
596
|
onOperationExecuted?: (operation: string, input: OperationInput) => void;
|
|
597
|
-
/** Defaults to a no-op (createVehicleHttpApp's own default) -- daemon.ts wires
|
|
597
|
+
/** Defaults to a no-op (createVehicleHttpApp's own default) -- daemon.ts wires log/log.ts's own `logger` so a failed invocation is actually logged, not silently discarded. */
|
|
598
598
|
logger?: Logger;
|
|
599
599
|
/**
|
|
600
600
|
* Backs GET /daemon/diagnose -- "who am I, and what happened recently" (see
|
|
@@ -56,6 +56,10 @@ export class InMemoryTaskMutationRequestStore implements TaskMutationRequestStor
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
put(record: TaskMutationRequestRecord): void {
|
|
59
|
+
// Checked in the same order sqlite-task-mutation-request-store.ts's own catch-and-reclassify
|
|
60
|
+
// does: a still-pending (taskId, operation) always becomes the more specific
|
|
61
|
+
// TaskMutationPendingError first, regardless of which underlying constraint actually
|
|
62
|
+
// collided (SQLite's partial unique pending index, or the PRIMARY KEY check below).
|
|
59
63
|
if (record.state === "pending" && record.taskId) {
|
|
60
64
|
const existing = this.findPending(record.taskId, record.operation, record.createdAt);
|
|
61
65
|
if (existing) {
|
|
@@ -66,7 +70,18 @@ export class InMemoryTaskMutationRequestStore implements TaskMutationRequestStor
|
|
|
66
70
|
);
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
|
-
|
|
73
|
+
// Mirror SQLite's own PRIMARY KEY (request_scope, idempotency_key): a genuine duplicate
|
|
74
|
+
// (scope, key) is always rejected, never silently overwritten -- SQLite throws on any such
|
|
75
|
+
// collision regardless of whether the colliding row's other columns match, so this does too,
|
|
76
|
+
// rather than only checking receiptId. Every real write path already dedupes via
|
|
77
|
+
// mutationRequests.get() before ever calling put() twice for the same (scope, key)
|
|
78
|
+
// (task-service.ts's prepareMutation()), so this is unreachable through normal application
|
|
79
|
+
// flow today -- it only guards a caller that talks to the store interface directly.
|
|
80
|
+
const recordKey = this.recordKey(record.scope, record.key);
|
|
81
|
+
if (this.records.has(recordKey)) {
|
|
82
|
+
throw new Error(`task mutation request already exists for scope "${record.scope}" key "${record.key}"`);
|
|
83
|
+
}
|
|
84
|
+
this.records.set(recordKey, { ...record });
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
complete(scope: string, key: string, responseJson: string, updatedAt: string): void {
|