better-ship 0.3.2

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,282 @@
1
+ import { panic } from "better-result";
2
+
3
+ //#region src/core/app-error.ts
4
+ /**
5
+ * The base of every expected failure. A subclass declares its tag and its classification,
6
+ * so no boundary has to guess what kind of failure it holds. A boundary tells our errors from
7
+ * raw throws with `instanceof AppError`; a value that fails it was never judged.
8
+ *
9
+ * A `readonly` field with a literal initializer keeps the literal type, so
10
+ * `readonly _tag = 'StoreUnavailable'` is enough for `Result` unions and `matchError`.
11
+ */
12
+ var AppError = class extends Error {
13
+ get name() {
14
+ return this._tag;
15
+ }
16
+ };
17
+
18
+ //#endregion
19
+ //#region src/core/defects.ts
20
+ /**
21
+ * Mark a line the types say cannot run. A union member without a branch stops compilation here.
22
+ *
23
+ * @throws Panic when a value outside the union arrives at runtime.
24
+ */
25
+ function unreachable(value) {
26
+ return panic(`Unreachable: ${String(value)}`);
27
+ }
28
+ /**
29
+ * Mark a body that is not written yet. A defect, not an expected failure.
30
+ *
31
+ * @param what - The behavior the body will provide, such as `invoice export`.
32
+ * @throws Panic always.
33
+ */
34
+ function notImplemented(what) {
35
+ return panic(`Not implemented: ${what}`);
36
+ }
37
+
38
+ //#endregion
39
+ //#region src/core/failure-classification.ts
40
+ /** Decide whether one retry owner may repeat a failed operation. */
41
+ function shouldRetryFailure(args) {
42
+ if (!args.repeatSafe) return false;
43
+ if (args.classification === "transient") return true;
44
+ if (args.classification === "terminal") return false;
45
+ return args.owner === "durable";
46
+ }
47
+
48
+ //#endregion
49
+ //#region src/core/transport-classification.ts
50
+ /**
51
+ * Messages that the platform-neutral transports raise for a failure that may pass.
52
+ *
53
+ * A boundary that knows its own runtime adds its own patterns before these.
54
+ */
55
+ const TRANSIENT_TRANSPORT_PATTERNS = [
56
+ /network/,
57
+ /fetch failed/,
58
+ /timeout/,
59
+ /timed?\s*out/,
60
+ /connection.*(lost|reset|refused|closed|aborted)/,
61
+ /econnreset/,
62
+ /econnrefused/,
63
+ /etimedout/,
64
+ /eai_again/
65
+ ];
66
+ /** Cancellation is a decision, not a failure to repeat. */
67
+ function isAbortError(cause) {
68
+ return cause instanceof Error && cause.name === "AbortError";
69
+ }
70
+ /** Match known transient transport messages while excluding explicit cancellation. */
71
+ function isTransientTransportError(cause) {
72
+ if (!(cause instanceof Error) || isAbortError(cause)) return false;
73
+ const message = cause.message.toLowerCase();
74
+ return TRANSIENT_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message));
75
+ }
76
+
77
+ //#endregion
78
+ //#region src/core/logger.ts
79
+ /**
80
+ * The supported log severity levels, in ascending order.
81
+ *
82
+ * A const object, not an enum, so a parsed environment value typed `'INFO'` is a `LogLevel`.
83
+ */
84
+ const LogLevel = {
85
+ DEBUG: "DEBUG",
86
+ INFO: "INFO",
87
+ WARN: "WARN",
88
+ ERROR: "ERROR"
89
+ };
90
+ const DEFAULT_SETTINGS = {
91
+ level: LogLevel.INFO,
92
+ format: "json"
93
+ };
94
+ let settings = DEFAULT_SETTINGS;
95
+ /**
96
+ * Set application settings, including for existing loggers. Per-logger overrides take priority.
97
+ * Call at startup, never per request. An omitted field resets to INFO and JSON output.
98
+ */
99
+ function configureLogger(config) {
100
+ settings = {
101
+ level: config.level ?? DEFAULT_SETTINGS.level,
102
+ format: config.format ?? DEFAULT_SETTINGS.format
103
+ };
104
+ }
105
+ let errorHook = null;
106
+ /**
107
+ * Register the function that receives each logged error.
108
+ *
109
+ * Set once at the process entry point. A later call replaces the earlier hook.
110
+ */
111
+ function setLoggerErrorHook(fn) {
112
+ errorHook = fn;
113
+ }
114
+ const CONSOLE_METHOD = {
115
+ [LogLevel.DEBUG]: "debug",
116
+ [LogLevel.INFO]: "info",
117
+ [LogLevel.WARN]: "warn",
118
+ [LogLevel.ERROR]: "error"
119
+ };
120
+ const ESC = String.fromCharCode(27);
121
+ const ANSI_RESET = `${ESC}[0m`;
122
+ const ANSI_GRAY = `${ESC}[90m`;
123
+ const ANSI_BLUE = `${ESC}[34m`;
124
+ const LEVEL_ANSI = {
125
+ DEBUG: `${ESC}[2;36m`,
126
+ INFO: ANSI_BLUE,
127
+ WARN: `${ESC}[33m`,
128
+ ERROR: `${ESC}[1;31m`
129
+ };
130
+ function prettyLine(entry) {
131
+ const level = `[${entry.level}]`;
132
+ const module = `[${entry.module}]`;
133
+ if ("window" in globalThis) return `${entry.timestamp} ${level} ${module} ${entry.message}`;
134
+ return `${`${ANSI_GRAY}${entry.timestamp}${ANSI_RESET}`} ${`${LEVEL_ANSI[entry.level]}${level}${ANSI_RESET}`} ${`${ANSI_BLUE}${module}${ANSI_RESET}`} ${entry.message}`;
135
+ }
136
+ /**
137
+ * JSON format writes the entry object. Pretty format writes one line, then the data,
138
+ * the error, and the stack as separate console arguments.
139
+ */
140
+ const consoleSink = (entry) => {
141
+ const write = console[CONSOLE_METHOD[entry.level]];
142
+ if (settings.format === "json") {
143
+ write(entry);
144
+ return;
145
+ }
146
+ const line = prettyLine(entry);
147
+ if (entry.error === void 0) {
148
+ write(line, ...entry.data);
149
+ return;
150
+ }
151
+ const { stack, ...error } = entry.error;
152
+ if (stack === void 0) write(line, ...entry.data, error);
153
+ else write(line, ...entry.data, error, `\n${stack}`);
154
+ };
155
+ let logSink = consoleSink;
156
+ /**
157
+ * Send every log entry somewhere other than `console`.
158
+ *
159
+ * Set once at the process entry point, never per module. A logger is created
160
+ * by name and nothing else, so its destination is a fact about the process.
161
+ */
162
+ function setLogSink(fn) {
163
+ logSink = fn;
164
+ }
165
+ const LOG_LEVELS = Object.values(LogLevel);
166
+ const MAX_ERROR_CAUSE_DEPTH = 3;
167
+ /**
168
+ * Diagnostics worth keeping off an error, named one by one.
169
+ *
170
+ * An allowlist rather than every own property: an error raised by a library we
171
+ * do not control may hang a request or a user payload off itself, and a log is
172
+ * the wrong place to discover that. `_tag` and `classification` are what every `AppError`
173
+ * declares. `remote` and the three flags after it are the ones workerd sets itself.
174
+ */
175
+ const KEPT_ERROR_FIELDS = [
176
+ "_tag",
177
+ "classification",
178
+ "code",
179
+ "operation",
180
+ "status",
181
+ "statusCode",
182
+ "remote",
183
+ "retryable",
184
+ "overloaded",
185
+ "durableObjectReset"
186
+ ];
187
+ const serializeError = (error, depth = 0) => {
188
+ if (depth > MAX_ERROR_CAUSE_DEPTH) return {
189
+ name: "Error",
190
+ message: "[cause chain truncated]"
191
+ };
192
+ const carrier = error;
193
+ const kept = KEPT_ERROR_FIELDS.filter((key) => carrier[key] !== void 0);
194
+ const serialized = {
195
+ name: error.name,
196
+ message: error.message
197
+ };
198
+ if (depth === 0 && error.stack !== void 0) serialized.stack = error.stack;
199
+ if (kept.length > 0) serialized.fields = Object.fromEntries(kept.map((key) => [key, carrier[key]]));
200
+ if (error.cause instanceof Error) serialized.cause = serializeError(error.cause, depth + 1);
201
+ else if (error.cause !== void 0) serialized.cause = {
202
+ name: "Error",
203
+ message: "[non-error cause]"
204
+ };
205
+ return serialized;
206
+ };
207
+ /** Write module-scoped logs with application defaults and optional overrides. */
208
+ var Logger = class {
209
+ module;
210
+ config;
211
+ /** Create a logger for one module. */
212
+ constructor(module, overrideConfig) {
213
+ this.module = module;
214
+ this.config = { ...overrideConfig };
215
+ }
216
+ log(level, message, data, error) {
217
+ const threshold = this.config.level ?? settings.level;
218
+ if (threshold === "OFF") return;
219
+ if (LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf(threshold)) return;
220
+ const entry = {
221
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
222
+ level,
223
+ module: this.module,
224
+ message,
225
+ data
226
+ };
227
+ logSink(error === void 0 ? entry : {
228
+ ...entry,
229
+ error
230
+ });
231
+ }
232
+ /** Write a debug log. */
233
+ debug(message, ...values) {
234
+ this.log(LogLevel.DEBUG, message, values);
235
+ }
236
+ /** Write an information log. */
237
+ info(message, ...values) {
238
+ this.log(LogLevel.INFO, message, values);
239
+ }
240
+ /** Write a warning log. */
241
+ warn(message, ...values) {
242
+ this.log(LogLevel.WARN, message, values);
243
+ }
244
+ /**
245
+ * Write an error log and send the error to the configured hook.
246
+ *
247
+ * The details say which call this was, and the error says what went wrong
248
+ * inside it. The entry carries the details as data and the error beside them.
249
+ */
250
+ error(message, context = {}) {
251
+ const details = context.details ?? {};
252
+ const thrown = context.error;
253
+ let error;
254
+ if (thrown instanceof Error) error = serializeError(thrown);
255
+ else if (thrown !== void 0) {
256
+ let json;
257
+ try {
258
+ json = JSON.stringify(thrown);
259
+ } catch {
260
+ json = "[non-serializable error]";
261
+ }
262
+ error = {
263
+ name: "NonError",
264
+ message: json
265
+ };
266
+ }
267
+ this.log(LogLevel.ERROR, message, context.details === void 0 ? [] : [details], error);
268
+ if (errorHook !== null && thrown !== void 0) errorHook({
269
+ error: thrown,
270
+ distinctId: context.userId,
271
+ context: details
272
+ });
273
+ }
274
+ };
275
+ /** Create a logger for one module. */
276
+ function createLogger(module, config) {
277
+ return new Logger(module, config);
278
+ }
279
+
280
+ //#endregion
281
+ export { createLogger as a, isAbortError as c, notImplemented as d, unreachable as f, consoleSink as i, isTransientTransportError as l, Logger as n, setLogSink as o, AppError as p, configureLogger as r, setLoggerErrorHook as s, LogLevel as t, shouldRetryFailure as u };
282
+ //# sourceMappingURL=core-D0asUCvq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-D0asUCvq.js","names":[],"sources":["../src/core/app-error.ts","../src/core/defects.ts","../src/core/failure-classification.ts","../src/core/transport-classification.ts","../src/core/logger.ts"],"sourcesContent":["import type { FailureClassification } from './failure-classification.ts'\n\n/**\n * The base of every expected failure. A subclass declares its tag and its classification,\n * so no boundary has to guess what kind of failure it holds. A boundary tells our errors from\n * raw throws with `instanceof AppError`; a value that fails it was never judged.\n *\n * A `readonly` field with a literal initializer keeps the literal type, so\n * `readonly _tag = 'StoreUnavailable'` is enough for `Result` unions and `matchError`.\n */\nexport abstract class AppError extends Error {\n abstract readonly _tag: string\n abstract readonly classification: FailureClassification\n\n override get name(): string {\n return this._tag\n }\n}\n","import { panic } from 'better-result'\n\n/**\n * Mark a line the types say cannot run. A union member without a branch stops compilation here.\n *\n * @throws Panic when a value outside the union arrives at runtime.\n */\nexport function unreachable(value: never): never {\n return panic(`Unreachable: ${String(value)}`)\n}\n\n/**\n * Mark a body that is not written yet. A defect, not an expected failure.\n *\n * @param what - The behavior the body will provide, such as `invoice export`.\n * @throws Panic always.\n */\nexport function notImplemented(what: string): never {\n return panic(`Not implemented: ${what}`)\n}\n","/**\n * What one failure is, before any policy decides what to do about it.\n *\n * `transient`: the world may differ on the next attempt. `terminal`: repeating gives the\n * same answer. `unknown`: nobody judged this error, so it came from outside unwrapped.\n */\nexport type FailureClassification = 'transient' | 'terminal' | 'unknown'\n\n/**\n * Who is asking to repeat the work.\n *\n * An immediate owner holds a caller and a socket open. A durable owner already stored the\n * work, so it can afford to try again on a failure nobody classified.\n */\nexport type RetryOwner = 'immediate' | 'durable'\n\n/** Decide whether one retry owner may repeat a failed operation. */\nexport function shouldRetryFailure(args: {\n readonly classification: FailureClassification\n readonly repeatSafe: boolean\n readonly owner: RetryOwner\n}): boolean {\n if (!args.repeatSafe) return false\n if (args.classification === 'transient') return true\n if (args.classification === 'terminal') return false\n return args.owner === 'durable'\n}\n","/**\n * Messages that the platform-neutral transports raise for a failure that may pass.\n *\n * A boundary that knows its own runtime adds its own patterns before these.\n */\nconst TRANSIENT_TRANSPORT_PATTERNS = [\n /network/,\n /fetch failed/,\n /timeout/,\n /timed?\\s*out/,\n /connection.*(lost|reset|refused|closed|aborted)/,\n /econnreset/,\n /econnrefused/,\n /etimedout/,\n /eai_again/,\n]\n\n/** Cancellation is a decision, not a failure to repeat. */\nexport function isAbortError(cause: unknown): boolean {\n return cause instanceof Error && cause.name === 'AbortError'\n}\n\n/** Match known transient transport messages while excluding explicit cancellation. */\nexport function isTransientTransportError(cause: unknown): boolean {\n if (!(cause instanceof Error) || isAbortError(cause)) return false\n const message = cause.message.toLowerCase()\n return TRANSIENT_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message))\n}\n","/**\n * Module-scoped logging for every host.\n *\n * A logger emits one plain-data entry per log. The default sink hands it to `console` as an\n * entry object for a log service or as a colored line for a person, selected at startup.\n */\n\n/** A value a log entry may carry. Errors enter only through `Logger.error`. */\nexport type LogValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | ReadonlyArray<LogValue>\n | LogFields\n\n/** Named log values. */\nexport type LogFields = { readonly [key: string]: LogValue }\n\n/**\n * The supported log severity levels, in ascending order.\n *\n * A const object, not an enum, so a parsed environment value typed `'INFO'` is a `LogLevel`.\n */\nexport const LogLevel = {\n DEBUG: 'DEBUG',\n INFO: 'INFO',\n WARN: 'WARN',\n ERROR: 'ERROR',\n} as const\nexport type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]\n\n/** An error as plain data: name, message, stack, allowlisted fields, and the cause chain. */\nexport type SerializedError = {\n readonly name: string\n readonly message: string\n readonly stack?: string\n readonly fields?: LogFields\n readonly cause?: SerializedError\n}\n\n/** One log, as the sink receives it. Plain data, safe to stringify. */\nexport type LogEntry = {\n readonly timestamp: string\n readonly level: LogLevel\n readonly module: string\n readonly message: string\n readonly data: ReadonlyArray<LogValue>\n /** Present on `Logger.error` entries that carried an error. */\n readonly error?: SerializedError\n}\n\n/** The context an error log carries. */\nexport type ErrorLogContext = {\n readonly error?: unknown\n readonly userId?: string\n readonly details?: LogFields\n}\n\n/** What a logger lets through: a level and everything above it, or nothing. */\nexport type LogThreshold = LogLevel | 'OFF'\n\n/** Overrides the application settings for one logger. */\nexport interface LoggerConfig {\n readonly level?: LogThreshold\n}\n\n/** `json` writes the entry object. `pretty` writes one readable line for a person. */\nexport type LogFormat = 'json' | 'pretty'\n\n/**\n * The application settings, set once at the composition root.\n *\n * An omitted field keeps its default. An explicit `undefined` is a compile error, so the root\n * parses an environment value before passing it. The logger reads no environment itself.\n */\nexport interface LoggerSettings extends LoggerConfig {\n readonly format?: LogFormat\n}\n\n// The production server setting, so a root that never configures still logs safely.\nconst DEFAULT_SETTINGS: Required<LoggerSettings> = {\n level: LogLevel.INFO,\n format: 'json',\n}\n\n// The application configures once at startup; loggers can exist before startup completes.\nlet settings = DEFAULT_SETTINGS\n\n/**\n * Set application settings, including for existing loggers. Per-logger overrides take priority.\n * Call at startup, never per request. An omitted field resets to INFO and JSON output.\n */\nexport function configureLogger(config: LoggerSettings): void {\n settings = {\n level: config.level ?? DEFAULT_SETTINGS.level,\n format: config.format ?? DEFAULT_SETTINGS.format,\n }\n}\n\n/** The error data sent to the configured error hook. */\nexport interface ErrorCaptureEntry {\n readonly error: unknown\n readonly distinctId: string | undefined\n readonly context: LogFields\n}\n\n/** A function that forwards one logged error to an error service. */\nexport type LoggerErrorHook = (entry: ErrorCaptureEntry) => void\n\n/** Where every log entry goes. */\nexport type LogSink = (entry: LogEntry) => void\n\nlet errorHook: LoggerErrorHook | null = null\n\n/**\n * Register the function that receives each logged error.\n *\n * Set once at the process entry point. A later call replaces the earlier hook.\n */\nexport function setLoggerErrorHook(fn: LoggerErrorHook): void {\n errorHook = fn\n}\n\n// One console method per level, so a host that filters by method can tell them apart.\nconst CONSOLE_METHOD = {\n [LogLevel.DEBUG]: 'debug',\n [LogLevel.INFO]: 'info',\n [LogLevel.WARN]: 'warn',\n [LogLevel.ERROR]: 'error',\n} as const satisfies Record<LogLevel, 'debug' | 'info' | 'warn' | 'error'>\n\n// Built from the code point so no raw control character sits in the source.\nconst ESC = String.fromCharCode(27)\nconst ANSI_RESET = `${ESC}[0m`\nconst ANSI_GRAY = `${ESC}[90m`\nconst ANSI_BLUE = `${ESC}[34m`\n\n// Severity reads at a glance: quiet levels stay dim, and an error is the only bold line.\nconst LEVEL_ANSI = {\n DEBUG: `${ESC}[2;36m`,\n INFO: ANSI_BLUE,\n WARN: `${ESC}[33m`,\n ERROR: `${ESC}[1;31m`,\n} satisfies Record<LogLevel, string>\n\nfunction prettyLine(entry: LogEntry): string {\n // Node prints debug and info exactly like log, so the level must be in the text too.\n const level = `[${entry.level}]`\n const module = `[${entry.module}]`\n // A browser console colors by method on its own; a terminal needs ANSI to do the same.\n if ('window' in globalThis) return `${entry.timestamp} ${level} ${module} ${entry.message}`\n const timestamp = `${ANSI_GRAY}${entry.timestamp}${ANSI_RESET}`\n const coloredLevel = `${LEVEL_ANSI[entry.level]}${level}${ANSI_RESET}`\n const coloredModule = `${ANSI_BLUE}${module}${ANSI_RESET}`\n return `${timestamp} ${coloredLevel} ${coloredModule} ${entry.message}`\n}\n\n/**\n * JSON format writes the entry object. Pretty format writes one line, then the data,\n * the error, and the stack as separate console arguments.\n */\nexport const consoleSink: LogSink = (entry) => {\n // Resolved per call, so a console replaced after import still receives the output.\n const write = console[CONSOLE_METHOD[entry.level]]\n if (settings.format === 'json') {\n write(entry)\n return\n }\n const line = prettyLine(entry)\n if (entry.error === undefined) {\n write(line, ...entry.data)\n return\n }\n // A stack inside an object prints as one quoted string; as its own argument it prints as lines.\n const { stack, ...error } = entry.error\n if (stack === undefined) write(line, ...entry.data, error)\n else write(line, ...entry.data, error, `\\n${stack}`)\n}\n\nlet logSink: LogSink = consoleSink\n\n/**\n * Send every log entry somewhere other than `console`.\n *\n * Set once at the process entry point, never per module. A logger is created\n * by name and nothing else, so its destination is a fact about the process.\n */\nexport function setLogSink(fn: LogSink): void {\n logSink = fn\n}\n\nconst LOG_LEVELS = Object.values(LogLevel)\nconst MAX_ERROR_CAUSE_DEPTH = 3\n\n/**\n * Diagnostics worth keeping off an error, named one by one.\n *\n * An allowlist rather than every own property: an error raised by a library we\n * do not control may hang a request or a user payload off itself, and a log is\n * the wrong place to discover that. `_tag` and `classification` are what every `AppError`\n * declares. `remote` and the three flags after it are the ones workerd sets itself.\n */\nconst KEPT_ERROR_FIELDS = [\n '_tag',\n 'classification',\n 'code',\n 'operation',\n 'status',\n 'statusCode',\n 'remote',\n 'retryable',\n 'overloaded',\n 'durableObjectReset',\n] as const\n\ntype ErrorDiagnostics = Error & { readonly [K in (typeof KEPT_ERROR_FIELDS)[number]]?: LogValue }\n\ntype MutableSerializedError = { -readonly [K in keyof SerializedError]: SerializedError[K] }\n\nconst serializeError = (error: Error, depth = 0): SerializedError => {\n if (depth > MAX_ERROR_CAUSE_DEPTH) return { name: 'Error', message: '[cause chain truncated]' }\n\n // Every field is optional, so an `Error` is already one of these.\n const carrier: ErrorDiagnostics = error\n const kept = KEPT_ERROR_FIELDS.filter((key) => carrier[key] !== undefined)\n const serialized: MutableSerializedError = { name: error.name, message: error.message }\n if (depth === 0 && error.stack !== undefined) serialized.stack = error.stack\n if (kept.length > 0)\n serialized.fields = Object.fromEntries(kept.map((key) => [key, carrier[key]]))\n if (error.cause instanceof Error) serialized.cause = serializeError(error.cause, depth + 1)\n else if (error.cause !== undefined)\n serialized.cause = { name: 'Error', message: '[non-error cause]' }\n return serialized\n}\n\n/** Write module-scoped logs with application defaults and optional overrides. */\nexport class Logger {\n private readonly config: LoggerConfig\n\n /** Create a logger for one module. */\n constructor(\n private readonly module: string,\n overrideConfig?: LoggerConfig,\n ) {\n this.config = { ...overrideConfig }\n }\n\n private log(\n level: LogLevel,\n message: string,\n data: ReadonlyArray<LogValue>,\n error?: SerializedError,\n ): void {\n const threshold = this.config.level ?? settings.level\n if (threshold === 'OFF') return\n if (LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf(threshold)) return\n\n const entry = { timestamp: new Date().toISOString(), level, module: this.module, message, data }\n logSink(error === undefined ? entry : { ...entry, error })\n }\n\n /** Write a debug log. */\n debug(message: string, ...values: ReadonlyArray<LogValue>): void {\n this.log(LogLevel.DEBUG, message, values)\n }\n\n /** Write an information log. */\n info(message: string, ...values: ReadonlyArray<LogValue>): void {\n this.log(LogLevel.INFO, message, values)\n }\n\n /** Write a warning log. */\n warn(message: string, ...values: ReadonlyArray<LogValue>): void {\n this.log(LogLevel.WARN, message, values)\n }\n\n /**\n * Write an error log and send the error to the configured hook.\n *\n * The details say which call this was, and the error says what went wrong\n * inside it. The entry carries the details as data and the error beside them.\n */\n error(message: string, context: ErrorLogContext = {}): void {\n const details = context.details ?? {}\n const thrown = context.error\n let error: SerializedError | undefined\n if (thrown instanceof Error) {\n error = serializeError(thrown)\n } else if (thrown !== undefined) {\n // Not an `Error`, so there is no name or stack to keep, only its JSON form.\n let json: string\n try {\n json = JSON.stringify(thrown)\n } catch {\n json = '[non-serializable error]'\n }\n error = { name: 'NonError', message: json }\n }\n this.log(LogLevel.ERROR, message, context.details === undefined ? [] : [details], error)\n\n if (errorHook !== null && thrown !== undefined) {\n errorHook({ error: thrown, distinctId: context.userId, context: details })\n }\n }\n}\n\n/** Create a logger for one module. */\nexport function createLogger(module: string, config?: LoggerConfig): Logger {\n return new Logger(module, config)\n}\n"],"mappings":"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"}
package/dist/core.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { C as unreachable, D as shouldRetryFailure, E as RetryOwner, S as notImplemented, T as FailureClassification, _ as createLogger, a as LogFormat, b as isAbortError, c as LogThreshold, d as LoggerConfig, f as LoggerErrorHook, g as consoleSink, h as configureLogger, i as LogFields, l as LogValue, m as SerializedError, n as ErrorLogContext, o as LogLevel, p as LoggerSettings, r as LogEntry, s as LogSink, t as ErrorCaptureEntry, u as Logger, v as setLogSink, w as AppError, x as isTransientTransportError, y as setLoggerErrorHook } from "./index-eL4vb02m.js";
2
+ export { AppError, type ErrorCaptureEntry, type ErrorLogContext, type FailureClassification, type LogEntry, type LogFields, type LogFormat, LogLevel, type LogSink, type LogThreshold, type LogValue, Logger, type LoggerConfig, type LoggerErrorHook, type LoggerSettings, type RetryOwner, type SerializedError, configureLogger, consoleSink, createLogger, isAbortError, isTransientTransportError, notImplemented, setLogSink, setLoggerErrorHook, shouldRetryFailure, unreachable };
package/dist/core.js ADDED
@@ -0,0 +1,3 @@
1
+ import { a as createLogger, c as isAbortError, d as notImplemented, f as unreachable, i as consoleSink, l as isTransientTransportError, n as Logger, o as setLogSink, p as AppError, r as configureLogger, s as setLoggerErrorHook, t as LogLevel, u as shouldRetryFailure } from "./core-D0asUCvq.js";
2
+
3
+ export { AppError, LogLevel, Logger, configureLogger, consoleSink, createLogger, isAbortError, isTransientTransportError, notImplemented, setLogSink, setLoggerErrorHook, shouldRetryFailure, unreachable };
@@ -0,0 +1,183 @@
1
+ //#region src/core/failure-classification.d.ts
2
+ /**
3
+ * What one failure is, before any policy decides what to do about it.
4
+ *
5
+ * `transient`: the world may differ on the next attempt. `terminal`: repeating gives the
6
+ * same answer. `unknown`: nobody judged this error, so it came from outside unwrapped.
7
+ */
8
+ type FailureClassification = 'transient' | 'terminal' | 'unknown';
9
+ /**
10
+ * Who is asking to repeat the work.
11
+ *
12
+ * An immediate owner holds a caller and a socket open. A durable owner already stored the
13
+ * work, so it can afford to try again on a failure nobody classified.
14
+ */
15
+ type RetryOwner = 'immediate' | 'durable';
16
+ /** Decide whether one retry owner may repeat a failed operation. */
17
+ declare function shouldRetryFailure(args: {
18
+ readonly classification: FailureClassification;
19
+ readonly repeatSafe: boolean;
20
+ readonly owner: RetryOwner;
21
+ }): boolean;
22
+ //#endregion
23
+ //#region src/core/app-error.d.ts
24
+ /**
25
+ * The base of every expected failure. A subclass declares its tag and its classification,
26
+ * so no boundary has to guess what kind of failure it holds. A boundary tells our errors from
27
+ * raw throws with `instanceof AppError`; a value that fails it was never judged.
28
+ *
29
+ * A `readonly` field with a literal initializer keeps the literal type, so
30
+ * `readonly _tag = 'StoreUnavailable'` is enough for `Result` unions and `matchError`.
31
+ */
32
+ declare abstract class AppError extends Error {
33
+ abstract readonly _tag: string;
34
+ abstract readonly classification: FailureClassification;
35
+ get name(): string;
36
+ }
37
+ //#endregion
38
+ //#region src/core/defects.d.ts
39
+ /**
40
+ * Mark a line the types say cannot run. A union member without a branch stops compilation here.
41
+ *
42
+ * @throws Panic when a value outside the union arrives at runtime.
43
+ */
44
+ declare function unreachable(value: never): never;
45
+ /**
46
+ * Mark a body that is not written yet. A defect, not an expected failure.
47
+ *
48
+ * @param what - The behavior the body will provide, such as `invoice export`.
49
+ * @throws Panic always.
50
+ */
51
+ declare function notImplemented(what: string): never;
52
+ //#endregion
53
+ //#region src/core/transport-classification.d.ts
54
+ /** Cancellation is a decision, not a failure to repeat. */
55
+ declare function isAbortError(cause: unknown): boolean;
56
+ /** Match known transient transport messages while excluding explicit cancellation. */
57
+ declare function isTransientTransportError(cause: unknown): boolean;
58
+ //#endregion
59
+ //#region src/core/logger.d.ts
60
+ /**
61
+ * Module-scoped logging for every host.
62
+ *
63
+ * A logger emits one plain-data entry per log. The default sink hands it to `console` as an
64
+ * entry object for a log service or as a colored line for a person, selected at startup.
65
+ */
66
+ /** A value a log entry may carry. Errors enter only through `Logger.error`. */
67
+ type LogValue = string | number | boolean | null | undefined | ReadonlyArray<LogValue> | LogFields;
68
+ /** Named log values. */
69
+ type LogFields = {
70
+ readonly [key: string]: LogValue;
71
+ };
72
+ /**
73
+ * The supported log severity levels, in ascending order.
74
+ *
75
+ * A const object, not an enum, so a parsed environment value typed `'INFO'` is a `LogLevel`.
76
+ */
77
+ declare const LogLevel: {
78
+ readonly DEBUG: 'DEBUG';
79
+ readonly INFO: 'INFO';
80
+ readonly WARN: 'WARN';
81
+ readonly ERROR: 'ERROR';
82
+ };
83
+ type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
84
+ /** An error as plain data: name, message, stack, allowlisted fields, and the cause chain. */
85
+ type SerializedError = {
86
+ readonly name: string;
87
+ readonly message: string;
88
+ readonly stack?: string;
89
+ readonly fields?: LogFields;
90
+ readonly cause?: SerializedError;
91
+ };
92
+ /** One log, as the sink receives it. Plain data, safe to stringify. */
93
+ type LogEntry = {
94
+ readonly timestamp: string;
95
+ readonly level: LogLevel;
96
+ readonly module: string;
97
+ readonly message: string;
98
+ readonly data: ReadonlyArray<LogValue>;
99
+ /** Present on `Logger.error` entries that carried an error. */
100
+ readonly error?: SerializedError;
101
+ };
102
+ /** The context an error log carries. */
103
+ type ErrorLogContext = {
104
+ readonly error?: unknown;
105
+ readonly userId?: string;
106
+ readonly details?: LogFields;
107
+ };
108
+ /** What a logger lets through: a level and everything above it, or nothing. */
109
+ type LogThreshold = LogLevel | 'OFF';
110
+ /** Overrides the application settings for one logger. */
111
+ interface LoggerConfig {
112
+ readonly level?: LogThreshold;
113
+ }
114
+ /** `json` writes the entry object. `pretty` writes one readable line for a person. */
115
+ type LogFormat = 'json' | 'pretty';
116
+ /**
117
+ * The application settings, set once at the composition root.
118
+ *
119
+ * An omitted field keeps its default. An explicit `undefined` is a compile error, so the root
120
+ * parses an environment value before passing it. The logger reads no environment itself.
121
+ */
122
+ interface LoggerSettings extends LoggerConfig {
123
+ readonly format?: LogFormat;
124
+ }
125
+ /**
126
+ * Set application settings, including for existing loggers. Per-logger overrides take priority.
127
+ * Call at startup, never per request. An omitted field resets to INFO and JSON output.
128
+ */
129
+ declare function configureLogger(config: LoggerSettings): void;
130
+ /** The error data sent to the configured error hook. */
131
+ interface ErrorCaptureEntry {
132
+ readonly error: unknown;
133
+ readonly distinctId: string | undefined;
134
+ readonly context: LogFields;
135
+ }
136
+ /** A function that forwards one logged error to an error service. */
137
+ type LoggerErrorHook = (entry: ErrorCaptureEntry) => void;
138
+ /** Where every log entry goes. */
139
+ type LogSink = (entry: LogEntry) => void;
140
+ /**
141
+ * Register the function that receives each logged error.
142
+ *
143
+ * Set once at the process entry point. A later call replaces the earlier hook.
144
+ */
145
+ declare function setLoggerErrorHook(fn: LoggerErrorHook): void;
146
+ /**
147
+ * JSON format writes the entry object. Pretty format writes one line, then the data,
148
+ * the error, and the stack as separate console arguments.
149
+ */
150
+ declare const consoleSink: LogSink;
151
+ /**
152
+ * Send every log entry somewhere other than `console`.
153
+ *
154
+ * Set once at the process entry point, never per module. A logger is created
155
+ * by name and nothing else, so its destination is a fact about the process.
156
+ */
157
+ declare function setLogSink(fn: LogSink): void;
158
+ /** Write module-scoped logs with application defaults and optional overrides. */
159
+ declare class Logger {
160
+ private readonly module;
161
+ private readonly config;
162
+ /** Create a logger for one module. */
163
+ constructor(module: string, overrideConfig?: LoggerConfig);
164
+ private log;
165
+ /** Write a debug log. */
166
+ debug(message: string, ...values: ReadonlyArray<LogValue>): void;
167
+ /** Write an information log. */
168
+ info(message: string, ...values: ReadonlyArray<LogValue>): void;
169
+ /** Write a warning log. */
170
+ warn(message: string, ...values: ReadonlyArray<LogValue>): void;
171
+ /**
172
+ * Write an error log and send the error to the configured hook.
173
+ *
174
+ * The details say which call this was, and the error says what went wrong
175
+ * inside it. The entry carries the details as data and the error beside them.
176
+ */
177
+ error(message: string, context?: ErrorLogContext): void;
178
+ }
179
+ /** Create a logger for one module. */
180
+ declare function createLogger(module: string, config?: LoggerConfig): Logger;
181
+ //#endregion
182
+ export { unreachable as C, shouldRetryFailure as D, RetryOwner as E, notImplemented as S, FailureClassification as T, createLogger as _, LogFormat as a, isAbortError as b, LogThreshold as c, LoggerConfig as d, LoggerErrorHook as f, consoleSink as g, configureLogger as h, LogFields as i, LogValue as l, SerializedError as m, ErrorLogContext as n, LogLevel as o, LoggerSettings as p, LogEntry as r, LogSink as s, ErrorCaptureEntry as t, Logger as u, setLogSink as v, AppError as w, isTransientTransportError as x, setLoggerErrorHook as y };
183
+ //# sourceMappingURL=index-eL4vb02m.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-eL4vb02m.d.ts","names":[],"sources":["../src/core/failure-classification.ts","../src/core/app-error.ts","../src/core/defects.ts","../src/core/transport-classification.ts","../src/core/logger.ts"],"mappings":";;;;;;;KAMY;;;;;;;KAQA;;iBAGI,mBAAmB;WACxB,gBAAgB;WAChB;WACA,OAAO;;;;;;;;;;;;uBCVI,iBAAiB;oBACnB;oBACA,gBAAgB;MAErB;;;;;;;;;iBCPC,YAAY;;;;;;;iBAUZ,eAAe;;;;iBCCf,aAAa;;iBAKb,0BAA0B;;;;;;;;;;KCf9B,0DAMR,cAAc,YACd;;KAGQ;YAAwB,cAAc;;;;;;;cAOrC;WACX;WACA;WACA;WACA;;KAEU,mBAAmB,uBAAuB;;KAG1C;WACD;WACA;WACA;WACA,SAAS;WACT,QAAQ;;;KAIP;WACD;WACA,OAAO;WACP;WACA;WACA,MAAM,cAAc;;WAEpB,QAAQ;;;KAIP;WACD;WACA;WACA,UAAU;;;KAIT,eAAe;;UAGV;WACN,QAAQ;;;KAIP;;;;;;;UAQK,uBAAuB;WAC7B,SAAS;;;;;;iBAgBJ,gBAAgB,QAAQ;;UAQvB;WACN;WACA;WACA,SAAS;;;KAIR,mBAAmB,OAAO;;KAG1B,WAAW,OAAO;;;;;;iBASd,mBAAmB,IAAI;;;;;cA0C1B,aAAa;;;;;;;iBA0BV,WAAW,IAAI;;cAiDlB;mBAKQ;mBAJF;;EAGjB,YACmB,gBACjB,iBAAiB;UAKX;;EAeR,MAAM,oBAAoB,QAAQ,cAAc;;EAKhD,KAAK,oBAAoB,QAAQ,cAAc;;EAK/C,KAAK,oBAAoB,QAAQ,cAAc;;;;;;;EAU/C,MAAM,iBAAiB,UAAS;;;iBAyBlB,aAAa,gBAAgB,SAAS,eAAe"}
@@ -0,0 +1 @@
1
+ export {}
File without changes
@@ -0,0 +1 @@
1
+ export {}
File without changes
package/dist/ui.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/ui.js ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "https://www.schemastore.org/package.json",
3
+ "name": "better-ship",
4
+ "version": "0.3.2",
5
+ "description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/alexander-zuev/better-ship.git",
10
+ "directory": "packages/better-ship"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "src"
15
+ ],
16
+ "type": "module",
17
+ "exports": {
18
+ "./application": "./dist/application.js",
19
+ "./cloudflare": "./dist/cloudflare.js",
20
+ "./core": "./dist/core.js",
21
+ "./postgres": "./dist/postgres.js",
22
+ "./tanstack": "./dist/tanstack.js",
23
+ "./ui": "./dist/ui.js",
24
+ "./package.json": "./package.json"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "better-result": "3.0.1"
31
+ },
32
+ "devDependencies": {
33
+ "@cloudflare/workers-types": "5.20260905.1",
34
+ "tsdown": "0.23.0",
35
+ "typescript": "7.0.2",
36
+ "vitest": "4.1.11"
37
+ },
38
+ "engines": {
39
+ "node": ">=22"
40
+ },
41
+ "scripts": {
42
+ "build": "tsdown",
43
+ "dev": "tsdown --watch",
44
+ "typecheck": "tsc --noEmit && tsc -p tests/tsconfig.json",
45
+ "lint": "oxlint . --quiet",
46
+ "lint:fix": "oxlint . --fix --quiet",
47
+ "test": "vitest run --passWithNoTests"
48
+ }
49
+ }
@@ -0,0 +1,22 @@
1
+ import type { Command, Event, Query } from './messages.ts'
2
+
3
+ /**
4
+ * Handles one command with the dependency container and returns the caller's value.
5
+ * A handler that writes inside a transaction opens the unit of work itself. Failures throw.
6
+ */
7
+ export type CommandHandler<TCommand extends Command, Deps, Value> = (
8
+ command: TCommand,
9
+ deps: Deps,
10
+ ) => Promise<Value>
11
+
12
+ /**
13
+ * Handles one query with the read-only view of the container.
14
+ * The view is a type over the same object, so a query cannot name a write capability.
15
+ */
16
+ export type QueryHandler<TQuery extends Query, QueryDeps, Value> = (
17
+ query: TQuery,
18
+ deps: QueryDeps,
19
+ ) => Promise<Value>
20
+
21
+ /** Completes one subscriber's work for an event. Failures throw. */
22
+ export type EventHandler<TEvent extends Event, Deps> = (event: TEvent, deps: Deps) => Promise<void>
@@ -0,0 +1,13 @@
1
+ export { DuplicateMessageError } from './message-bus.errors.ts'
2
+ export type { CommandHandler, EventHandler, QueryHandler } from './handlers.ts'
3
+ export type { Command, Event, Message, MessageId, Query } from './messages.ts'
4
+ export { MessageBus, type IMessageBus } from './message-bus.ts'
5
+ export type {
6
+ CommandRegistry,
7
+ EventRegistry,
8
+ Handlers,
9
+ MessageRegistry,
10
+ MessageResult,
11
+ QueryRegistry,
12
+ RegistryMessage,
13
+ } from './registry.ts'
@@ -0,0 +1,16 @@
1
+ import { AppError } from '@/core'
2
+
3
+ import type { MessageId } from './messages.ts'
4
+
5
+ /**
6
+ * The unit of work throws this when a message's effects already committed.
7
+ * The bus counts an event subscriber that throws it as delivered. From a command it reaches the caller.
8
+ */
9
+ export class DuplicateMessageError extends AppError {
10
+ readonly _tag = 'DuplicateMessageError'
11
+ readonly classification = 'terminal'
12
+
13
+ constructor(readonly messageId: MessageId) {
14
+ super(`message ${messageId} already committed`)
15
+ }
16
+ }