@loutrejs/loutre 0.9.2 → 0.10.1

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.
Files changed (145) hide show
  1. package/dist/adapters/aws-lambda.d.ts +1 -1
  2. package/dist/adapters/aws-lambda.js +8 -2
  3. package/dist/adapters/bun.d.ts +3 -2
  4. package/dist/adapters/bun.js +12 -6
  5. package/dist/adapters/cloudflare-workers.d.ts +1 -1
  6. package/dist/adapters/cloudflare-workers.js +2 -1
  7. package/dist/adapters/electron.d.ts +1 -1
  8. package/dist/adapters/electron.js +8 -51
  9. package/dist/adapters/server-transports.js +2 -2
  10. package/dist/adapters/websocket-native.js +4 -1
  11. package/dist/application/definition.d.ts +4 -7
  12. package/dist/application/definition.js +3 -1
  13. package/dist/application/kernel.d.ts +0 -2
  14. package/dist/application/kernel.js +1 -3
  15. package/dist/core/args.d.ts +1 -0
  16. package/dist/core/args.js +2 -1
  17. package/dist/core/env.d.ts +1 -0
  18. package/dist/core/env.js +2 -1
  19. package/dist/core/extension.d.ts +23 -13
  20. package/dist/core/extension.js +3 -1
  21. package/dist/core/generic-layer.d.ts +9 -2
  22. package/dist/core/generic-layer.js +2 -1
  23. package/dist/core/identity.d.ts +18 -0
  24. package/dist/core/identity.js +5 -0
  25. package/dist/core/index.d.ts +13 -5
  26. package/dist/core/index.js +7 -1
  27. package/dist/core/injection.d.ts +2 -1
  28. package/dist/core/instrumentation.d.ts +7 -5
  29. package/dist/core/lifecycle.d.ts +4 -0
  30. package/dist/core/logger-identity.d.ts +4 -0
  31. package/dist/core/logger-identity.js +9 -0
  32. package/dist/core/logger-internal.d.ts +5 -0
  33. package/dist/core/logger-internal.js +27 -0
  34. package/dist/core/logger-module.d.ts +5 -0
  35. package/dist/core/logger-module.js +8 -0
  36. package/dist/core/logger.d.ts +42 -0
  37. package/dist/core/logger.js +221 -0
  38. package/dist/core/model/context.d.ts +6 -3
  39. package/dist/core/model/context.js +17 -17
  40. package/dist/core/model/identity.d.ts +19 -0
  41. package/dist/core/model/identity.js +8 -0
  42. package/dist/core/model/lifecycle.d.ts +3 -2
  43. package/dist/core/model/lifecycle.js +7 -3
  44. package/dist/core/model/logger.d.ts +3 -0
  45. package/dist/core/model/logger.js +59 -0
  46. package/dist/core/model/modules.d.ts +4 -1
  47. package/dist/core/model/modules.js +6 -4
  48. package/dist/core/model/providers.d.ts +1 -1
  49. package/dist/core/model/providers.js +8 -8
  50. package/dist/core/model/types.d.ts +25 -18
  51. package/dist/core/model/validation.js +3 -2
  52. package/dist/core/model/visibility.js +7 -2
  53. package/dist/core/model.js +15 -7
  54. package/dist/core/module.d.ts +7 -7
  55. package/dist/core/module.js +0 -3
  56. package/dist/core/runtime-input.d.ts +2 -1
  57. package/dist/core/source-location.d.ts +14 -2
  58. package/dist/core/source-location.js +8 -3
  59. package/dist/core/token-internal.js +3 -1
  60. package/dist/core/token.d.ts +4 -2
  61. package/dist/core/token.js +3 -0
  62. package/dist/core/units.d.ts +22 -0
  63. package/dist/core/units.js +14 -0
  64. package/dist/devtools/capsule.d.ts +17 -13
  65. package/dist/devtools/capsule.js +14 -10
  66. package/dist/devtools/decoder.d.ts +15 -0
  67. package/dist/devtools/decoder.js +179 -0
  68. package/dist/devtools/identity-internal.d.ts +6 -0
  69. package/dist/devtools/identity-internal.js +5 -0
  70. package/dist/devtools/identity.d.ts +21 -0
  71. package/dist/devtools/identity.js +7 -0
  72. package/dist/devtools/index.d.ts +3 -0
  73. package/dist/devtools/index.js +2 -0
  74. package/dist/devtools/inspector.d.ts +18 -16
  75. package/dist/devtools/observer.d.ts +32 -28
  76. package/dist/devtools/runtime.js +34 -30
  77. package/dist/devtools/values.js +3 -5
  78. package/dist/generated/version.d.ts +1 -1
  79. package/dist/generated/version.js +1 -1
  80. package/dist/graph/model.d.ts +25 -14
  81. package/dist/http/auth.d.ts +4 -2
  82. package/dist/http/client.d.ts +6 -3
  83. package/dist/http/client.js +8 -9
  84. package/dist/http/cors-internal.d.ts +7 -5
  85. package/dist/http/cors-internal.js +8 -6
  86. package/dist/http/extension.d.ts +60 -21
  87. package/dist/http/extension.js +91 -20
  88. package/dist/http/identity.d.ts +20 -0
  89. package/dist/http/identity.js +10 -0
  90. package/dist/http/index.d.ts +6 -2
  91. package/dist/http/index.js +3 -1
  92. package/dist/http/openapi-identity.d.ts +9 -0
  93. package/dist/http/openapi-identity.js +4 -0
  94. package/dist/http/openapi.d.ts +1 -1
  95. package/dist/http/openapi.js +10 -7
  96. package/dist/http/path.d.ts +8 -5
  97. package/dist/http/route-validation.js +2 -1
  98. package/dist/http/runtime/request.d.ts +3 -2
  99. package/dist/http/runtime/request.js +1 -4
  100. package/dist/http/websocket-upgrade.d.ts +2 -0
  101. package/dist/http/websocket-upgrade.js +4 -0
  102. package/dist/internal/brand.d.ts +8 -0
  103. package/dist/internal/brand.js +1 -0
  104. package/dist/message-port/extension.d.ts +4 -2
  105. package/dist/message-port/extension.js +15 -8
  106. package/dist/message-port/identity.d.ts +9 -0
  107. package/dist/message-port/identity.js +16 -0
  108. package/dist/message-port/index.d.ts +2 -0
  109. package/dist/message-port/index.js +1 -0
  110. package/dist/message-port/transport.d.ts +4 -2
  111. package/dist/message-port/transport.js +6 -1
  112. package/dist/runtime/async-iterator-lifecycle.d.ts +2 -1
  113. package/dist/runtime/async-iterator-lifecycle.js +2 -1
  114. package/dist/runtime/capability.d.ts +6 -2
  115. package/dist/runtime/capability.js +3 -1
  116. package/dist/runtime/di.d.ts +1 -3
  117. package/dist/runtime/di.js +62 -35
  118. package/dist/runtime/error.d.ts +3 -1
  119. package/dist/runtime/index.d.ts +6 -2
  120. package/dist/runtime/index.js +3 -1
  121. package/dist/runtime/kernel.d.ts +0 -2
  122. package/dist/runtime/kernel.js +68 -30
  123. package/dist/runtime/logger-source.d.ts +4 -0
  124. package/dist/runtime/logger-source.js +47 -0
  125. package/dist/runtime/logger.d.ts +2 -38
  126. package/dist/runtime/logger.js +1 -209
  127. package/dist/runtime/node-capabilities.d.ts +1 -1
  128. package/dist/runtime/node-capabilities.js +2 -1
  129. package/dist/runtime/server-port.d.ts +5 -1
  130. package/dist/runtime/server-port.js +2 -1
  131. package/dist/runtime/shutdown-hooks.d.ts +1 -1
  132. package/dist/runtime/shutdown-hooks.js +5 -4
  133. package/dist/tasks/extension.d.ts +18 -16
  134. package/dist/tasks/extension.js +28 -23
  135. package/dist/tasks/identity.d.ts +9 -0
  136. package/dist/tasks/identity.js +4 -0
  137. package/dist/tasks/index.d.ts +1 -0
  138. package/dist/tasks/runtime/cron.d.ts +6 -2
  139. package/dist/tasks/runtime/cron.js +9 -0
  140. package/dist/websocket/extension.d.ts +19 -9
  141. package/dist/websocket/extension.js +103 -30
  142. package/dist/websocket/identity.d.ts +9 -0
  143. package/dist/websocket/identity.js +4 -0
  144. package/dist/websocket/index.d.ts +1 -0
  145. package/package.json +1 -1
@@ -1,38 +1,2 @@
1
- import { frameworkProvidedTokenBrand } from '../core/token-internal.js';
2
- export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
3
- export interface LogRecord {
4
- readonly timestamp: string;
5
- readonly level: LogLevel;
6
- readonly message: string;
7
- readonly [key: string]: unknown;
8
- }
9
- export interface LoggerBackend {
10
- write(record: LogRecord): void;
11
- }
12
- export interface ConsoleLoggerBackendOptions {
13
- readonly colors?: boolean;
14
- readonly prefix?: string;
15
- }
16
- export declare class ConsoleLoggerBackend implements LoggerBackend {
17
- #private;
18
- constructor(options?: ConsoleLoggerBackendOptions);
19
- write(record: LogRecord): void;
20
- }
21
- export declare class JsonConsoleLoggerBackend implements LoggerBackend {
22
- write(record: LogRecord): void;
23
- }
24
- export declare class Logger {
25
- #private;
26
- readonly backend: LoggerBackend;
27
- readonly context: Readonly<Record<string, unknown>>;
28
- static readonly [frameworkProvidedTokenBrand] = true;
29
- constructor(backend?: LoggerBackend, context?: Readonly<Record<string, unknown>>);
30
- child(context: Readonly<Record<string, unknown>>): Logger;
31
- debug(message: string, context?: Readonly<Record<string, unknown>>): void;
32
- info(message: string, context?: Readonly<Record<string, unknown>>): void;
33
- warn(message: string, context?: Readonly<Record<string, unknown>>): void;
34
- error(message: string, context?: Readonly<Record<string, unknown>>): void;
35
- }
36
- export declare class SilentLogger extends Logger {
37
- constructor(context?: Readonly<Record<string, unknown>>);
38
- }
1
+ export { ConsoleLoggerBackend, JsonConsoleLoggerBackend, LoggerService, LoggerToken, SilentLogger, } from '../core/logger.js';
2
+ export type { ConsoleLoggerBackendOptions, LoggerBackend, LogLevel, LogRecord, } from '../core/logger.js';
@@ -1,209 +1 @@
1
- import { frameworkProvidedTokenBrand } from '../core/token-internal.js';
2
- export class ConsoleLoggerBackend {
3
- #colors;
4
- #prefix;
5
- constructor(options = {}) {
6
- this.#colors = options.colors ?? detectConsoleColors();
7
- this.#prefix = options.prefix ?? 'Loutre';
8
- }
9
- write(record) {
10
- writeToConsole(record.level, formatConsoleLogRecord(record, this.#prefix, this.#colors));
11
- }
12
- }
13
- export class JsonConsoleLoggerBackend {
14
- write(record) {
15
- writeToConsole(record.level, serializeLogRecord(record));
16
- }
17
- }
18
- class SilentLoggerBackend {
19
- write(_record) { }
20
- }
21
- export class Logger {
22
- backend;
23
- context;
24
- static [frameworkProvidedTokenBrand] = true;
25
- constructor(backend = new ConsoleLoggerBackend(), context = {}) {
26
- this.backend = backend;
27
- this.context = context;
28
- }
29
- child(context) {
30
- return new Logger(this.backend, { ...this.context, ...context });
31
- }
32
- debug(message, context = {}) {
33
- this.#write('debug', message, context);
34
- }
35
- info(message, context = {}) {
36
- this.#write('info', message, context);
37
- }
38
- warn(message, context = {}) {
39
- this.#write('warn', message, context);
40
- }
41
- error(message, context = {}) {
42
- this.#write('error', message, context);
43
- }
44
- #write(level, message, context) {
45
- this.backend.write({
46
- ...this.context,
47
- ...context,
48
- timestamp: new Date().toISOString(),
49
- level,
50
- message,
51
- });
52
- }
53
- }
54
- export class SilentLogger extends Logger {
55
- constructor(context = {}) {
56
- super(new SilentLoggerBackend(), context);
57
- }
58
- }
59
- function serializeLogRecord(record) {
60
- const seen = new WeakSet();
61
- try {
62
- return JSON.stringify(record, (_key, value) => {
63
- if (typeof value === 'bigint')
64
- return value.toString();
65
- if (value instanceof Error) {
66
- return {
67
- name: value.name,
68
- message: value.message,
69
- ...(value.stack === undefined ? {} : { stack: value.stack }),
70
- ...(value.cause === undefined ? {} : { cause: value.cause }),
71
- };
72
- }
73
- if (value !== null && typeof value === 'object') {
74
- if (seen.has(value))
75
- return '[Circular]';
76
- seen.add(value);
77
- }
78
- return value;
79
- });
80
- }
81
- catch (error) {
82
- return JSON.stringify({
83
- timestamp: record.timestamp,
84
- level: 'error',
85
- message: 'Log record serialization failed',
86
- serializationError: error instanceof Error ? error.message : String(error),
87
- });
88
- }
89
- }
90
- function writeToConsole(level, value) {
91
- const target = level === 'error'
92
- ? console.error
93
- : level === 'warn'
94
- ? console.warn
95
- : console.log;
96
- target(value);
97
- }
98
- function formatConsoleLogRecord(record, prefix, colors) {
99
- const { timestamp, level, message, ...recordContext } = record;
100
- const context = { ...recordContext };
101
- const source = typeof context.source === 'string'
102
- ? context.source
103
- : typeof context.protocol === 'string'
104
- ? context.protocol
105
- : undefined;
106
- if (source !== undefined)
107
- delete context.source;
108
- const runtimeProcess = getRuntimeProcess();
109
- const processLabel = runtimeProcess?.pid === undefined
110
- ? `[${prefix}]`
111
- : `[${prefix}] ${runtimeProcess.pid}`;
112
- const levelLabel = level === 'info' ? 'LOG' : level.toUpperCase();
113
- const levelColor = level === 'error' ? 31 : level === 'warn' ? 33 : level === 'debug' ? 36 : 32;
114
- const sourceLabel = source === undefined ? '' : `${paint(`[${source}]`, 33, colors)} `;
115
- const formattedContext = formatLogContext(context);
116
- const contextSuffix = formattedContext === '' ? '' : ` ${paint(formattedContext, 90, colors)}`;
117
- return `${paint(processLabel, 32, colors)} - ${formatLocalTimestamp(timestamp)} ${paint(levelLabel.padStart(7), levelColor, colors)} ${sourceLabel}${paint(message, levelColor, colors)}${contextSuffix}`;
118
- }
119
- function formatLogContext(context) {
120
- const entries = Object.entries(context);
121
- if (entries.length === 0)
122
- return '';
123
- return `{ ${entries
124
- .map(([key, value]) => `${formatLogKey(key)}=${formatLogValue(value)}`)
125
- .join(', ')} }`;
126
- }
127
- function formatLogKey(key) {
128
- return /^[A-Za-z_$][\w$]*$/u.test(key) ? key : JSON.stringify(key);
129
- }
130
- function formatLogValue(value, ancestors = new WeakSet()) {
131
- try {
132
- if (value === null)
133
- return 'null';
134
- if (value === undefined)
135
- return 'undefined';
136
- if (typeof value === 'string') {
137
- return /^[\w./:@+-]+$/u.test(value) ? value : JSON.stringify(value);
138
- }
139
- if (typeof value === 'number' || typeof value === 'boolean')
140
- return String(value);
141
- if (typeof value === 'bigint')
142
- return `${value}n`;
143
- if (typeof value === 'symbol')
144
- return String(value);
145
- if (typeof value === 'function')
146
- return `[Function ${value.name || 'anonymous'}]`;
147
- if (ancestors.has(value))
148
- return '[Circular]';
149
- ancestors.add(value);
150
- try {
151
- if (value instanceof Date)
152
- return value.toISOString();
153
- if (value instanceof Error) {
154
- return formatLogValue({
155
- name: value.name,
156
- message: value.message,
157
- ...(value.stack === undefined ? {} : { stack: value.stack }),
158
- ...(value.cause === undefined ? {} : { cause: value.cause }),
159
- }, ancestors);
160
- }
161
- if (Array.isArray(value)) {
162
- return `[${value.map((item) => formatLogValue(item, ancestors)).join(', ')}]`;
163
- }
164
- return `{ ${Object.entries(value)
165
- .map(([key, item]) => `${formatLogKey(key)}=${formatLogValue(item, ancestors)}`)
166
- .join(', ')} }`;
167
- }
168
- finally {
169
- ancestors.delete(value);
170
- }
171
- }
172
- catch (error) {
173
- const message = error instanceof Error ? error.message : String(error);
174
- return `[Unformattable: ${message}]`;
175
- }
176
- }
177
- function formatLocalTimestamp(timestamp) {
178
- const date = new Date(timestamp);
179
- if (Number.isNaN(date.getTime()))
180
- return timestamp;
181
- return ([
182
- date.getFullYear(),
183
- String(date.getMonth() + 1).padStart(2, '0'),
184
- String(date.getDate()).padStart(2, '0'),
185
- ].join('/') +
186
- ` ${[
187
- String(date.getHours()).padStart(2, '0'),
188
- String(date.getMinutes()).padStart(2, '0'),
189
- String(date.getSeconds()).padStart(2, '0'),
190
- ].join(':')}.${String(date.getMilliseconds()).padStart(3, '0')}`);
191
- }
192
- function paint(value, color, enabled) {
193
- return enabled ? `\u001B[${color}m${value}\u001B[39m` : value;
194
- }
195
- function getRuntimeProcess() {
196
- return globalThis.process;
197
- }
198
- function detectConsoleColors() {
199
- const runtimeProcess = getRuntimeProcess();
200
- const environment = runtimeProcess?.env;
201
- if (environment?.NO_COLOR !== undefined ||
202
- environment?.NODE_DISABLE_COLORS !== undefined) {
203
- return false;
204
- }
205
- if (environment?.FORCE_COLOR !== undefined) {
206
- return environment.FORCE_COLOR !== '0';
207
- }
208
- return runtimeProcess?.stdout?.isTTY === true;
209
- }
1
+ export { ConsoleLoggerBackend, JsonConsoleLoggerBackend, LoggerService, LoggerToken, SilentLogger, } from '../core/logger.js';
@@ -1,5 +1,5 @@
1
1
  /** CLIが`@loutrejs/node`へ依存するとBunでもNode.js専用moduleを評価するため、Capability metadataだけをruntime-neutral側へ分離する。 */
2
2
  export declare const nodeRuntimeSupport: {
3
3
  readonly runtime: 'node';
4
- readonly capabilities: Set<string>;
4
+ readonly capabilities: ReadonlySet<import("./capability.js").RuntimeSupportFeature>;
5
5
  };
@@ -1,7 +1,8 @@
1
+ import { runtimeSupportFeatures } from './capability.js';
1
2
  /** CLIが`@loutrejs/node`へ依存するとBunでもNode.js専用moduleを評価するため、Capability metadataだけをruntime-neutral側へ分離する。 */
2
3
  export const nodeRuntimeSupport = {
3
4
  runtime: 'node',
4
- capabilities: new Set([
5
+ capabilities: runtimeSupportFeatures([
5
6
  'http.server',
6
7
  'websocket.server',
7
8
  'http.request.streaming',
@@ -1,2 +1,6 @@
1
- export declare function initialServerPort(requestedPort: number | undefined): number;
1
+ import type { Brand } from '../internal/brand.js';
2
+ export type RequestedPort = Brand<number, 'port.requested'>;
3
+ export type BoundPort = Brand<number, 'port.bound'>;
4
+ export declare const boundPort: (value: number) => BoundPort;
5
+ export declare function initialServerPort(requestedPort: number | undefined): RequestedPort;
2
6
  export declare function canRetryOnNextPort(error: unknown, port: number): boolean;
@@ -1,5 +1,6 @@
1
+ export const boundPort = (value) => value;
1
2
  export function initialServerPort(requestedPort) {
2
- return requestedPort ?? 3000;
3
+ return (requestedPort ?? 3000);
3
4
  }
4
5
  export function canRetryOnNextPort(error, port) {
5
6
  return port < 65_535 && isAddressInUseError(error);
@@ -6,7 +6,7 @@ export interface ProcessSignalSource {
6
6
  export interface ProcessShutdownHooksOptions {
7
7
  readonly source: ProcessSignalSource;
8
8
  readonly close: (signal: ShutdownSignal) => Promise<void>;
9
- readonly onError: (error: unknown, signal: ShutdownSignal) => void;
9
+ readonly onError: (error: unknown, signal: ShutdownSignal) => void | Promise<void>;
10
10
  }
11
11
  export declare class ProcessShutdownHooks {
12
12
  #private;
@@ -43,7 +43,8 @@ export class ProcessShutdownHooks {
43
43
  return;
44
44
  }
45
45
  catch (error) {
46
- this.#reportError(error, signal);
46
+ // ログ送信の完了待ちでshutdownの再試行を止めない。
47
+ void this.#reportError(error, signal);
47
48
  if (!(error instanceof ApplicationShutdownError) ||
48
49
  error.safeCleanupBoundaryReached ||
49
50
  !error.retryAfter)
@@ -52,15 +53,15 @@ export class ProcessShutdownHooks {
52
53
  await error.retryAfter;
53
54
  }
54
55
  catch (retryError) {
55
- this.#reportError(retryError, signal);
56
+ void this.#reportError(retryError, signal);
56
57
  return;
57
58
  }
58
59
  }
59
60
  }
60
61
  }
61
- #reportError(error, signal) {
62
+ async #reportError(error, signal) {
62
63
  try {
63
- this.#options.onError(error, signal);
64
+ await this.#options.onError(error, signal);
64
65
  }
65
66
  catch (reportingError) {
66
67
  console.error('Runtime shutdown error reporter failed', error, reportingError);
@@ -1,3 +1,5 @@
1
+ import { type DurationMs } from '../core/units.js';
2
+ import { type TaskExecutionKey, type TaskName, type TriggerName, type QueueName } from './identity.js';
1
3
  import { type ExecutionDefinition, type ProviderDescriptor, type SchemaOutput, type StandardSchemaV1, type Token } from '../core/index.js';
2
4
  export type TaskRuntime<TInput, TOutput> = [TInput] extends [void] ? () => TOutput | Promise<TOutput> : (input: TInput) => TOutput | Promise<TOutput>;
3
5
  export interface TaskDefinitionData<TInput = unknown, TOutput = unknown> {
@@ -13,7 +15,7 @@ export type TaskArguments<TTask> = [TaskInput<TTask>] extends [void] ? readonly
13
15
  declare const taskExecutionIdentityKey: unique symbol;
14
16
  declare const queueDriverToken: unique symbol;
15
17
  export interface TaskExecutionIdentity {
16
- readonly [taskExecutionIdentityKey]: symbol;
18
+ readonly [taskExecutionIdentityKey]: TaskExecutionKey;
17
19
  }
18
20
  type AnyTaskDefinition = ExecutionDefinition & TaskExecutionIdentity & {
19
21
  readonly type: 'task';
@@ -39,7 +41,7 @@ export interface FixedDelayTriggerDefinitionData<TTask extends AnyTaskDefinition
39
41
  }
40
42
  export interface QueueDescriptor<TSchema extends StandardSchemaV1 = StandardSchemaV1, TName extends string = string> {
41
43
  readonly kind: 'queue';
42
- readonly name: TName;
44
+ readonly name: QueueName<TName>;
43
45
  readonly payload: TSchema;
44
46
  readonly [queueDriverToken]: Token<QueueConsumerDriver>;
45
47
  }
@@ -62,39 +64,39 @@ export type TriggerDefinitionData = CronTriggerDefinitionData | FixedDelayTrigge
62
64
  type TasksDefinitionData = (TaskDefinitionData & TaskExecutionIdentity) | TriggerDefinitionData;
63
65
  export interface TasksCompiledTask {
64
66
  readonly type: 'task';
65
- readonly taskExecutionId: symbol;
66
- readonly name: string;
67
+ readonly taskExecutionId: TaskExecutionKey;
68
+ readonly name: TaskName;
67
69
  readonly factory: () => (...arguments_: any[]) => any;
68
70
  }
69
71
  export interface TasksCompiledCronTrigger {
70
72
  readonly type: 'cron';
71
- readonly name: string;
73
+ readonly name: TriggerName;
72
74
  readonly expression: string;
73
75
  readonly timezone: string;
74
76
  readonly overlap: CronOverlap;
75
- readonly taskExecutionId: symbol;
76
- readonly taskName: string;
77
+ readonly taskExecutionId: TaskExecutionKey;
78
+ readonly taskName: TaskName;
77
79
  }
78
80
  export interface TasksCompiledFixedDelayTrigger {
79
81
  readonly type: 'fixed-delay';
80
- readonly name: string;
81
- readonly delay: number;
82
+ readonly name: TriggerName;
83
+ readonly delay: DurationMs;
82
84
  readonly immediate: boolean;
83
- readonly taskExecutionId: symbol;
84
- readonly taskName: string;
85
+ readonly taskExecutionId: TaskExecutionKey;
86
+ readonly taskName: TaskName;
85
87
  }
86
88
  export interface TasksCompiledQueueConsumerTrigger {
87
89
  readonly type: 'queue-consumer';
88
- readonly name: string;
89
- readonly queueName: string;
90
+ readonly name: TriggerName;
91
+ readonly queueName: QueueName;
90
92
  readonly queuePayload: StandardSchemaV1;
91
93
  readonly queueDriver: Token<QueueConsumerDriver>;
92
- readonly taskExecutionId: symbol;
93
- readonly taskName: string;
94
+ readonly taskExecutionId: TaskExecutionKey;
95
+ readonly taskName: TaskName;
94
96
  }
95
97
  export type TasksCompiledExecution = TasksCompiledTask | TasksCompiledCronTrigger | TasksCompiledFixedDelayTrigger | TasksCompiledQueueConsumerTrigger;
96
98
  export interface TasksExtensionRuntime {
97
- run<TInput, TOutput>(taskExecutionId: symbol, taskName: string, ...arguments_: [TInput] extends [void] ? readonly [] : readonly [input: TInput]): Promise<TOutput>;
99
+ run<TInput, TOutput>(taskExecutionId: TaskExecutionKey, taskName: string, ...arguments_: [TInput] extends [void] ? readonly [] : readonly [input: TInput]): Promise<TOutput>;
98
100
  startTriggers(): Promise<void>;
99
101
  stopTriggers(): Promise<void>;
100
102
  drain(): Promise<void>;
@@ -1,5 +1,8 @@
1
+ import { dependencyConsumerId } from '../core/identity.js';
2
+ import { milliseconds } from '../core/units.js';
3
+ import { taskExecutionKey, taskName as compiledTaskName, triggerName, queueName, } from './identity.js';
1
4
  import { collectInjectedDependencies, defineExecution, defineExecutionExtension, provide, runInInjectionContext, token, validateSchema, } from '../core/index.js';
2
- import { isValidCronExpression, matchesCronTrigger } from './runtime/cron.js';
5
+ import { isValidCronExpression, matchesCronTrigger, parseCronSchedule, } from './runtime/cron.js';
3
6
  import { match } from 'ts-pattern';
4
7
  const taskExecutionIdentityKey = Symbol.for('loutre.tasks.task-execution-identity');
5
8
  const queueDriverToken = Symbol.for('loutre.tasks.queue-driver');
@@ -15,14 +18,14 @@ export const tasksExtension = defineExecutionExtension({
15
18
  executionKind: 'task.invocation',
16
19
  dependencies: collectInjectedDependencies({
17
20
  kind: 'execution',
18
- id: `task:${candidate.name}`,
19
- name: candidate.name,
21
+ id: dependencyConsumerId(`task:${candidate.name}`),
22
+ name: compiledTaskName(candidate.name),
20
23
  }, () => candidate.factory()),
21
24
  capabilities: [],
22
25
  compiled: Object.freeze({
23
26
  type: 'task',
24
27
  taskExecutionId: candidate[taskExecutionIdentityKey],
25
- name: candidate.name,
28
+ name: compiledTaskName(candidate.name),
26
29
  factory: candidate.factory,
27
30
  }),
28
31
  }))
@@ -34,12 +37,12 @@ export const tasksExtension = defineExecutionExtension({
34
37
  capabilities: [],
35
38
  compiled: Object.freeze({
36
39
  type: 'cron',
37
- name: candidate.name,
40
+ name: triggerName(candidate.name),
38
41
  expression: candidate.expression,
39
42
  timezone: candidate.timezone,
40
43
  overlap: candidate.overlap,
41
44
  taskExecutionId: candidate.task[taskExecutionIdentityKey],
42
- taskName: candidate.task.name,
45
+ taskName: compiledTaskName(candidate.task.name),
43
46
  }),
44
47
  }))
45
48
  .with({ type: 'fixed-delay' }, (candidate) => ({
@@ -50,11 +53,11 @@ export const tasksExtension = defineExecutionExtension({
50
53
  capabilities: [],
51
54
  compiled: Object.freeze({
52
55
  type: 'fixed-delay',
53
- name: candidate.name,
54
- delay: candidate.delay,
56
+ name: triggerName(candidate.name),
57
+ delay: milliseconds(candidate.delay),
55
58
  immediate: candidate.immediate,
56
59
  taskExecutionId: candidate.task[taskExecutionIdentityKey],
57
- taskName: candidate.task.name,
60
+ taskName: compiledTaskName(candidate.task.name),
58
61
  }),
59
62
  }))
60
63
  .with({ type: 'queue-consumer' }, (candidate) => ({
@@ -65,12 +68,12 @@ export const tasksExtension = defineExecutionExtension({
65
68
  capabilities: [],
66
69
  compiled: Object.freeze({
67
70
  type: 'queue-consumer',
68
- name: candidate.name,
71
+ name: triggerName(candidate.name),
69
72
  queueName: candidate.queue.name,
70
73
  queuePayload: candidate.queue.payload,
71
74
  queueDriver: queueRuntimeToken(candidate.queue),
72
75
  taskExecutionId: candidate.task[taskExecutionIdentityKey],
73
- taskName: candidate.task.name,
76
+ taskName: compiledTaskName(candidate.task.name),
74
77
  }),
75
78
  }))
76
79
  .exhaustive();
@@ -142,7 +145,7 @@ export const tasksExtension = defineExecutionExtension({
142
145
  .with({ type: 'fixed-delay' }, (candidate) => ({
143
146
  type: 'fixed-delay',
144
147
  name: candidate.name,
145
- delay: candidate.delay,
148
+ delay: milliseconds(candidate.delay),
146
149
  immediate: candidate.immediate,
147
150
  task: candidate.taskName,
148
151
  }))
@@ -168,7 +171,7 @@ export const tasksExtension = defineExecutionExtension({
168
171
  },
169
172
  });
170
173
  export function task(definition) {
171
- const taskExecutionId = Symbol(`loutre.tasks.execution:${definition.name}`);
174
+ const taskExecutionId = taskExecutionKey(Symbol(`loutre.tasks.execution:${definition.name}`));
172
175
  return defineExecution(tasksExtension, {
173
176
  type: 'task',
174
177
  name: definition.name,
@@ -209,7 +212,7 @@ export function queue(declaration) {
209
212
  }
210
213
  return Object.freeze({
211
214
  kind: 'queue',
212
- name: declaration.name,
215
+ name: queueName(declaration.name),
213
216
  payload: declaration.payload,
214
217
  [queueDriverToken]: token(`loutre.tasks.queue-driver.${declaration.name}`),
215
218
  });
@@ -230,7 +233,6 @@ export function consume(declaration) {
230
233
  }
231
234
  function createTasksRuntime(bindings, applicationRuntime) {
232
235
  const runtimes = new Map();
233
- const runtimeExecutionIds = new Map();
234
236
  const triggers = [];
235
237
  for (const { execution, resolve } of bindings) {
236
238
  const compiled = execution.compiled;
@@ -241,13 +243,15 @@ function createTasksRuntime(bindings, applicationRuntime) {
241
243
  const runtime = runInInjectionContext({
242
244
  consumer: {
243
245
  kind: 'execution',
244
- id: `task:${compiled.name}`,
246
+ id: dependencyConsumerId(`task:${compiled.name}`),
245
247
  name: compiled.name,
246
248
  },
247
249
  resolve: (dependency, consumer) => resolve(dependency, { consumer }),
248
250
  }, () => compiled.factory());
249
- runtimes.set(compiled.taskExecutionId, runtime);
250
- runtimeExecutionIds.set(compiled.taskExecutionId, execution.id);
251
+ runtimes.set(compiled.taskExecutionId, {
252
+ invoke: runtime,
253
+ executionId: execution.id,
254
+ });
251
255
  }
252
256
  let triggerHandles = [];
253
257
  let triggerStartup;
@@ -264,7 +268,7 @@ function createTasksRuntime(bindings, applicationRuntime) {
264
268
  return Promise.reject(new Error(`LUTRE_TASK_NOT_REGISTERED: ${taskName}`));
265
269
  }
266
270
  const invocation = (async () => {
267
- const executionId = runtimeExecutionIds.get(taskExecutionId) ?? taskName;
271
+ const executionId = runtime.executionId;
268
272
  const hasInput = arguments_.length > 0;
269
273
  const input = hasInput ? arguments_[0] : undefined;
270
274
  const lease = applicationRuntime.beginExecution({
@@ -279,7 +283,7 @@ function createTasksRuntime(bindings, applicationRuntime) {
279
283
  let result;
280
284
  let hasResult = false;
281
285
  try {
282
- const invoke = () => Reflect.apply(runtime, undefined, arguments_);
286
+ const invoke = () => Reflect.apply(runtime.invoke, undefined, arguments_);
283
287
  result = await (lease.run ? lease.run(invoke) : invoke());
284
288
  hasResult = true;
285
289
  return result;
@@ -416,13 +420,14 @@ async function startTrigger(trigger, run, resolve) {
416
420
  }
417
421
  function startCronTrigger(trigger, run) {
418
422
  let stopped = false;
423
+ const schedule = parseCronSchedule(trigger.expression, trigger.timezone);
419
424
  let lastMinute;
420
425
  const active = new Set();
421
426
  const tick = () => {
422
427
  if (stopped)
423
428
  return;
424
429
  const now = new Date();
425
- if (!matchesCronTrigger(trigger, now))
430
+ if (!matchesCronTrigger(schedule, now))
426
431
  return;
427
432
  const minute = Math.floor(now.getTime() / 60_000);
428
433
  if (lastMinute === minute)
@@ -506,9 +511,9 @@ async function startQueueTrigger(trigger, run, resolve) {
506
511
  function taskExecutionIdentity(definition) {
507
512
  if (taskExecutionIdentityKey in definition &&
508
513
  typeof definition[taskExecutionIdentityKey] === 'symbol') {
509
- return definition[taskExecutionIdentityKey];
514
+ return taskExecutionKey(definition[taskExecutionIdentityKey]);
510
515
  }
511
- return Symbol('loutre.tasks.unregistered');
516
+ return taskExecutionKey(Symbol('loutre.tasks.unregistered'));
512
517
  }
513
518
  function tasksStateError(state) {
514
519
  return new Error(`LUTRE_TASKS_${state.toUpperCase()}`);
@@ -0,0 +1,9 @@
1
+ import type { Brand } from '../internal/brand.js';
2
+ export type TaskExecutionKey = Brand<symbol, 'tasks.execution-key'>;
3
+ export type TaskName = Brand<string, 'tasks.task-name'>;
4
+ export type TriggerName = Brand<string, 'tasks.trigger-name'>;
5
+ export type QueueName<TName extends string = string> = Brand<TName, 'tasks.queue-name'>;
6
+ export declare const taskExecutionKey: (value: symbol) => TaskExecutionKey;
7
+ export declare const taskName: (value: string) => TaskName;
8
+ export declare const triggerName: (value: string) => TriggerName;
9
+ export declare const queueName: <const TName extends string>(value: TName) => QueueName<TName>;
@@ -0,0 +1,4 @@
1
+ export const taskExecutionKey = (value) => value;
2
+ export const taskName = (value) => value;
3
+ export const triggerName = (value) => value;
4
+ export const queueName = (value) => value;
@@ -1,2 +1,3 @@
1
1
  export { bindQueueDriver, consume, cron, fixedDelay, queue, task, tasksExtension, } from './extension.js';
2
2
  export type { CronOverlap, CronTriggerDefinition, CronTriggerDefinitionData, FixedDelayTriggerDefinition, FixedDelayTriggerDefinitionData, QueueConsumerDriver, QueueConsumerHandle, QueueConsumerTriggerDefinition, QueueConsumerTriggerDefinitionData, QueueDescriptor, QueuePayload, TaskArguments, TaskDefinition, TaskDefinitionData, TaskExecutionIdentity, TaskInput, TaskOutput, TaskRuntime, TasksCompiledCronTrigger, TasksCompiledExecution, TasksCompiledFixedDelayTrigger, TasksCompiledQueueConsumerTrigger, TasksCompiledTask, TasksExtensionRuntime, TasksHostApi, TriggerDefinition, TriggerDefinitionData, } from './extension.js';
3
+ export type { TaskExecutionKey, TaskName, TriggerName, QueueName, } from './identity.js';
@@ -1,6 +1,10 @@
1
+ import type { Brand } from '../../internal/brand.js';
2
+ export type CronExpression = Brand<string, 'tasks.cron-expression'>;
3
+ export type IanaTimeZone = Brand<string, 'tasks.iana-timezone'>;
1
4
  export interface CronSchedule {
2
- readonly expression: string;
3
- readonly timezone: string;
5
+ readonly expression: CronExpression;
6
+ readonly timezone: IanaTimeZone;
4
7
  }
5
8
  export declare function matchesCronTrigger(trigger: CronSchedule, instant: Date): boolean;
6
9
  export declare function isValidCronExpression(expression: string): boolean;
10
+ export declare function parseCronSchedule(expression: string, timezone: string): CronSchedule;
@@ -87,3 +87,12 @@ function isValidCronField(field, minimum, maximum) {
87
87
  return start >= minimum && end <= maximum && start <= end;
88
88
  });
89
89
  }
90
+ export function parseCronSchedule(expression, timezone) {
91
+ if (!isValidCronExpression(expression))
92
+ throw new TypeError('portableな5-field cron expressionが必要です。');
93
+ new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format();
94
+ return Object.freeze({
95
+ expression: expression,
96
+ timezone: timezone,
97
+ });
98
+ }