@zintrust/core 0.1.41 → 0.1.42
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 +17 -1
- package/src/boot/bootstrap.js +27 -11
- package/src/boot/registry/runtime.d.ts.map +1 -1
- package/src/boot/registry/runtime.js +11 -0
- package/src/cli/CLI.d.ts.map +1 -1
- package/src/cli/CLI.js +12 -0
- package/src/cli/commands/ConfigCommand.d.ts.map +1 -1
- package/src/cli/commands/ConfigCommand.js +3 -5
- package/src/cli/commands/D1LearnCommand.d.ts +9 -0
- package/src/cli/commands/D1LearnCommand.d.ts.map +1 -0
- package/src/cli/commands/D1LearnCommand.js +143 -0
- package/src/cli/commands/D1MigrateCommand.d.ts.map +1 -1
- package/src/cli/commands/D1MigrateCommand.js +55 -16
- package/src/cli/commands/InitContainerCommand.d.ts.map +1 -1
- package/src/cli/commands/InitContainerCommand.js +21 -6
- package/src/cli/commands/InitEcosystemCommand.d.ts +6 -0
- package/src/cli/commands/InitEcosystemCommand.d.ts.map +1 -0
- package/src/cli/commands/InitEcosystemCommand.js +51 -0
- package/src/cli/commands/MigrateCommand.d.ts.map +1 -1
- package/src/cli/commands/MigrateCommand.js +78 -36
- package/src/cli/commands/MigrateWorkerCommand.d.ts.map +1 -1
- package/src/cli/commands/MigrateWorkerCommand.js +36 -2
- package/src/cli/commands/PutCommand.d.ts +6 -0
- package/src/cli/commands/PutCommand.d.ts.map +1 -0
- package/src/cli/commands/PutCommand.js +173 -0
- package/src/cli/commands/QueueRecoveryCommand.d.ts.map +1 -1
- package/src/cli/commands/QueueRecoveryCommand.js +113 -14
- package/src/cli/commands/ScheduleListCommand.d.ts +6 -0
- package/src/cli/commands/ScheduleListCommand.d.ts.map +1 -0
- package/src/cli/commands/ScheduleListCommand.js +62 -0
- package/src/cli/commands/ScheduleRunCommand.d.ts +6 -0
- package/src/cli/commands/ScheduleRunCommand.d.ts.map +1 -0
- package/src/cli/commands/ScheduleRunCommand.js +32 -0
- package/src/cli/commands/ScheduleStartCommand.d.ts +6 -0
- package/src/cli/commands/ScheduleStartCommand.d.ts.map +1 -0
- package/src/cli/commands/ScheduleStartCommand.js +40 -0
- package/src/cli/commands/SecretsCommand.d.ts.map +1 -1
- package/src/cli/commands/SecretsCommand.js +2 -2
- package/src/cli/commands/schedule/ScheduleCliSupport.d.ts +6 -0
- package/src/cli/commands/schedule/ScheduleCliSupport.d.ts.map +1 -0
- package/src/cli/commands/schedule/ScheduleCliSupport.js +55 -0
- package/src/cli/config/ConfigManager.d.ts.map +1 -1
- package/src/cli/config/ConfigManager.js +8 -1
- package/src/cli/d1/D1SqlMigrations.d.ts.map +1 -1
- package/src/cli/d1/D1SqlMigrations.js +11 -1
- package/src/cli/d1/WranglerConfig.d.ts.map +1 -1
- package/src/cli/d1/WranglerConfig.js +34 -2
- package/src/cli/services/VersionChecker.d.ts.map +1 -1
- package/src/cli/services/VersionChecker.js +5 -1
- package/src/cli/utils/DatabaseCliUtils.d.ts.map +1 -1
- package/src/cli/utils/DatabaseCliUtils.js +6 -1
- package/src/cli/utils/EnvFileLoader.d.ts.map +1 -1
- package/src/cli/utils/EnvFileLoader.js +33 -14
- package/src/cli.d.ts +5 -0
- package/src/cli.d.ts.map +1 -0
- package/src/cli.js +4 -0
- package/src/collections/index.d.ts +2 -2
- package/src/collections/index.d.ts.map +1 -1
- package/src/collections/index.js +1 -1
- package/src/common/RemoteSignedJson.d.ts.map +1 -1
- package/src/common/RemoteSignedJson.js +49 -23
- package/src/common/utility.d.ts.map +1 -1
- package/src/common/utility.js +2 -6
- package/src/config/cloudflare.d.ts.map +1 -1
- package/src/config/cloudflare.js +19 -8
- package/src/config/env.js +2 -2
- package/src/helper/index.d.ts +225 -0
- package/src/helper/index.d.ts.map +1 -0
- package/src/helper/index.js +347 -0
- package/src/index.d.ts +3 -6
- package/src/index.d.ts.map +1 -1
- package/src/index.js +7 -9
- package/src/migrations/MigrationDiscovery.d.ts.map +1 -1
- package/src/migrations/MigrationDiscovery.js +2 -1
- package/src/orm/DatabaseAdapter.d.ts +1 -0
- package/src/orm/DatabaseAdapter.d.ts.map +1 -1
- package/src/orm/SchemaStatemenWriter.d.ts +15 -0
- package/src/orm/SchemaStatemenWriter.d.ts.map +1 -0
- package/src/orm/SchemaStatemenWriter.js +78 -0
- package/src/orm/adapters/D1Adapter.d.ts.map +1 -1
- package/src/orm/adapters/D1Adapter.js +52 -2
- package/src/orm/adapters/D1RemoteAdapter.d.ts.map +1 -1
- package/src/orm/adapters/D1RemoteAdapter.js +137 -89
- package/src/orm/adapters/MySQLProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/MySQLProxyAdapter.js +100 -81
- package/src/orm/adapters/PostgreSQLProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/PostgreSQLProxyAdapter.js +26 -10
- package/src/orm/adapters/SqlProxyAdapterUtils.d.ts.map +1 -1
- package/src/orm/adapters/SqlProxyAdapterUtils.js +2 -1
- package/src/orm/adapters/SqlProxyRegistryMode.d.ts +12 -0
- package/src/orm/adapters/SqlProxyRegistryMode.d.ts.map +1 -0
- package/src/orm/adapters/SqlProxyRegistryMode.js +24 -0
- package/src/orm/adapters/SqlServerProxyAdapter.d.ts +3 -0
- package/src/orm/adapters/SqlServerProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/SqlServerProxyAdapter.js +125 -117
- package/src/orm/migrations/MigrationStore.js +1 -1
- package/src/proxy/ProxyRequestParsing.d.ts +9 -0
- package/src/proxy/ProxyRequestParsing.d.ts.map +1 -0
- package/src/proxy/ProxyRequestParsing.js +16 -0
- package/src/proxy/RequestValidator.d.ts.map +1 -1
- package/src/proxy/RequestValidator.js +2 -1
- package/src/proxy/SigningService.js +2 -2
- package/src/proxy/SqlProxyDbOverrides.d.ts +17 -0
- package/src/proxy/SqlProxyDbOverrides.d.ts.map +1 -0
- package/src/proxy/SqlProxyDbOverrides.js +1 -0
- package/src/proxy/SqlProxyServerDeps.d.ts +12 -0
- package/src/proxy/SqlProxyServerDeps.d.ts.map +1 -0
- package/src/proxy/SqlProxyServerDeps.js +9 -0
- package/src/proxy/StatementPayloadValidator.d.ts +13 -0
- package/src/proxy/StatementPayloadValidator.d.ts.map +1 -0
- package/src/proxy/StatementPayloadValidator.js +18 -0
- package/src/proxy/StatementRegistryLoader.d.ts +2 -0
- package/src/proxy/StatementRegistryLoader.d.ts.map +1 -0
- package/src/proxy/StatementRegistryLoader.js +36 -0
- package/src/proxy/StatementRegistryResolver.d.ts +15 -0
- package/src/proxy/StatementRegistryResolver.d.ts.map +1 -0
- package/src/proxy/StatementRegistryResolver.js +34 -0
- package/src/proxy/d1/ZintrustD1Proxy.d.ts +2 -1
- package/src/proxy/d1/ZintrustD1Proxy.d.ts.map +1 -1
- package/src/proxy/d1/ZintrustD1Proxy.js +2 -1
- package/src/proxy/isMutatingSql.d.ts +2 -0
- package/src/proxy/isMutatingSql.d.ts.map +1 -0
- package/src/proxy/isMutatingSql.js +12 -0
- package/src/proxy/kv/ZintrustKvProxy.d.ts +2 -1
- package/src/proxy/kv/ZintrustKvProxy.d.ts.map +1 -1
- package/src/proxy/kv/ZintrustKvProxy.js +2 -1
- package/src/proxy/mysql/MySqlProxyServer.d.ts +2 -8
- package/src/proxy/mysql/MySqlProxyServer.d.ts.map +1 -1
- package/src/proxy/mysql/MySqlProxyServer.js +84 -51
- package/src/proxy/postgres/PostgresProxyServer.d.ts +2 -8
- package/src/proxy/postgres/PostgresProxyServer.d.ts.map +1 -1
- package/src/proxy/postgres/PostgresProxyServer.js +86 -48
- package/src/proxy/smtp/SmtpProxyServer.d.ts.map +1 -1
- package/src/proxy/smtp/SmtpProxyServer.js +6 -5
- package/src/proxy/sqlserver/SqlServerProxyServer.d.ts +2 -8
- package/src/proxy/sqlserver/SqlServerProxyServer.d.ts.map +1 -1
- package/src/proxy/sqlserver/SqlServerProxyServer.js +84 -49
- package/src/proxy.d.ts +4 -0
- package/src/proxy.d.ts.map +1 -0
- package/src/proxy.js +3 -0
- package/src/scheduler/Schedule.d.ts +36 -0
- package/src/scheduler/Schedule.d.ts.map +1 -0
- package/src/scheduler/Schedule.js +197 -0
- package/src/scheduler/ScheduleHttpGateway.d.ts +8 -0
- package/src/scheduler/ScheduleHttpGateway.d.ts.map +1 -0
- package/src/scheduler/ScheduleHttpGateway.js +196 -0
- package/src/scheduler/ScheduleRunner.d.ts +6 -0
- package/src/scheduler/ScheduleRunner.d.ts.map +1 -1
- package/src/scheduler/ScheduleRunner.js +166 -29
- package/src/scheduler/SchedulerRuntime.d.ts +15 -0
- package/src/scheduler/SchedulerRuntime.d.ts.map +1 -0
- package/src/scheduler/SchedulerRuntime.js +79 -0
- package/src/scheduler/cron/Cron.d.ts +19 -0
- package/src/scheduler/cron/Cron.d.ts.map +1 -0
- package/src/scheduler/cron/Cron.js +200 -0
- package/src/scheduler/leader/SchedulerLeader.d.ts +14 -0
- package/src/scheduler/leader/SchedulerLeader.d.ts.map +1 -0
- package/src/scheduler/leader/SchedulerLeader.js +187 -0
- package/src/scheduler/state/ScheduleStateStore.d.ts +27 -0
- package/src/scheduler/state/ScheduleStateStore.d.ts.map +1 -0
- package/src/scheduler/state/ScheduleStateStore.js +27 -0
- package/src/scheduler/types.d.ts +10 -0
- package/src/scheduler/types.d.ts.map +1 -1
- package/src/schedules/index.d.ts +1 -0
- package/src/schedules/index.d.ts.map +1 -1
- package/src/schedules/index.js +1 -0
- package/src/schedules/job-tracking-cleanup.d.ts +4 -0
- package/src/schedules/job-tracking-cleanup.d.ts.map +1 -0
- package/src/schedules/job-tracking-cleanup.js +116 -0
- package/src/schedules/log-cleanup.d.ts +1 -2
- package/src/schedules/log-cleanup.d.ts.map +1 -1
- package/src/schedules/log-cleanup.js +12 -15
- package/src/security/Sanitizer.d.ts.map +1 -1
- package/src/security/Sanitizer.js +1 -9
- package/src/security/SignedRequest.d.ts.map +1 -1
- package/src/security/SignedRequest.js +2 -2
- package/src/templates/docker/docker-compose.ecosystem.yml.tpl +301 -0
- package/src/templates/docker/docker-compose.schedules.yml.tpl +84 -0
- package/src/templates/project/basic/app/Schedules/index.ts.tpl +0 -0
- package/src/templates/project/basic/config/database.ts.tpl +1 -1
- package/src/toolkit/Secrets/Manifest.d.ts.map +1 -1
- package/src/toolkit/Secrets/Manifest.js +5 -7
- package/src/tools/mail/drivers/Smtp.d.ts.map +1 -1
- package/src/tools/mail/drivers/Smtp.js +7 -1
- package/src/tools/queue/JobReconciliationRunner.d.ts.map +1 -1
- package/src/tools/queue/JobReconciliationRunner.js +7 -39
- package/src/tools/queue/JobRecoveryDaemon.d.ts.map +1 -1
- package/src/tools/queue/JobRecoveryDaemon.js +116 -18
- package/src/tools/queue/JobStateTracker.d.ts +10 -1
- package/src/tools/queue/JobStateTracker.d.ts.map +1 -1
- package/src/tools/queue/JobStateTracker.js +24 -2
- package/src/tools/queue/JobStateTrackerDbPersistence.d.ts.map +1 -1
- package/src/tools/queue/JobStateTrackerDbPersistence.js +93 -2
|
@@ -4,45 +4,184 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Logger } from '../config/logger.js';
|
|
6
6
|
import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
7
|
+
import { Cron } from './cron/Cron.js';
|
|
8
|
+
import { InMemoryScheduleStateStore, } from './state/ScheduleStateStore.js';
|
|
9
|
+
const nowMs = () => Date.now();
|
|
10
|
+
const randomInt = (min, maxInclusive) => {
|
|
11
|
+
if (!Number.isFinite(min) || !Number.isFinite(maxInclusive))
|
|
12
|
+
return 0;
|
|
13
|
+
if (maxInclusive <= min)
|
|
14
|
+
return Math.floor(min);
|
|
15
|
+
return Math.floor(min + Math.random() * (maxInclusive - min + 1)); //NOSONAR
|
|
16
|
+
};
|
|
17
|
+
const resolveBackoffDelayMs = (state) => {
|
|
18
|
+
const policy = state.schedule.backoff;
|
|
19
|
+
if (!policy)
|
|
20
|
+
return 0;
|
|
21
|
+
const initialMs = Number.isFinite(policy.initialMs)
|
|
22
|
+
? Math.max(0, Math.floor(policy.initialMs))
|
|
23
|
+
: 0;
|
|
24
|
+
const maxMs = Number.isFinite(policy.maxMs) ? Math.max(0, Math.floor(policy.maxMs)) : 0;
|
|
25
|
+
if (initialMs <= 0 || maxMs <= 0)
|
|
26
|
+
return 0;
|
|
27
|
+
const factor = policy.factor === undefined || !Number.isFinite(policy.factor) || policy.factor <= 1
|
|
28
|
+
? 2
|
|
29
|
+
: policy.factor;
|
|
30
|
+
const power = Math.max(0, state.consecutiveFailures - 1);
|
|
31
|
+
const raw = initialMs * Math.pow(factor, power);
|
|
32
|
+
return Math.min(maxMs, Math.floor(raw));
|
|
33
|
+
};
|
|
34
|
+
const resolveJitterMs = (jitterMs) => {
|
|
35
|
+
return typeof jitterMs === 'number' && jitterMs > 0 ? randomInt(0, Math.floor(jitterMs)) : 0;
|
|
36
|
+
};
|
|
37
|
+
const computeBackoffDelay = (state) => {
|
|
38
|
+
if (state.schedule.enabled === false)
|
|
39
|
+
return null;
|
|
40
|
+
const backoffDelayMs = resolveBackoffDelayMs(state);
|
|
41
|
+
if (backoffDelayMs > 0) {
|
|
42
|
+
const jitter = resolveJitterMs(state.schedule.jitterMs);
|
|
43
|
+
return backoffDelayMs + jitter;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
const computeCronDelay = (schedule) => {
|
|
48
|
+
if (typeof schedule.cron !== 'string' || schedule.cron.trim().length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const tz = typeof schedule.timezone === 'string' && schedule.timezone.trim().length > 0
|
|
52
|
+
? schedule.timezone
|
|
53
|
+
: 'UTC';
|
|
54
|
+
const nextAt = Cron.nextRunAtMs(nowMs(), schedule.cron, tz);
|
|
55
|
+
const baseDelay = Math.max(0, nextAt - nowMs());
|
|
56
|
+
const jitter = resolveJitterMs(schedule.jitterMs);
|
|
57
|
+
return baseDelay + jitter;
|
|
58
|
+
};
|
|
59
|
+
const computeIntervalDelay = (schedule) => {
|
|
60
|
+
if (typeof schedule.intervalMs !== 'number' || schedule.intervalMs <= 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const base = Math.floor(schedule.intervalMs);
|
|
64
|
+
const jitter = resolveJitterMs(schedule.jitterMs);
|
|
65
|
+
return base + jitter;
|
|
66
|
+
};
|
|
67
|
+
const computeNextDelayMs = (state, outcome) => {
|
|
68
|
+
const schedule = state.schedule;
|
|
69
|
+
if (schedule.enabled === false)
|
|
70
|
+
return null;
|
|
71
|
+
if (outcome === 'failure') {
|
|
72
|
+
const backoffDelay = computeBackoffDelay(state);
|
|
73
|
+
if (backoffDelay !== null)
|
|
74
|
+
return backoffDelay;
|
|
75
|
+
}
|
|
76
|
+
const cronDelay = computeCronDelay(schedule);
|
|
77
|
+
if (cronDelay !== null)
|
|
78
|
+
return cronDelay;
|
|
79
|
+
const intervalDelay = computeIntervalDelay(schedule);
|
|
80
|
+
if (intervalDelay !== null)
|
|
81
|
+
return intervalDelay;
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
84
|
+
const clearTimer = (state) => {
|
|
85
|
+
if (state.timeoutId !== undefined) {
|
|
86
|
+
globalThis.clearTimeout(state.timeoutId);
|
|
87
|
+
state.timeoutId = undefined;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const scheduleNext = (state, kernel, invoke, outcome, store) => {
|
|
91
|
+
clearTimer(state);
|
|
92
|
+
const delay = computeNextDelayMs(state, outcome);
|
|
93
|
+
if (delay === null)
|
|
94
|
+
return;
|
|
95
|
+
const nextRunAt = nowMs() + delay;
|
|
96
|
+
void store.set(state.schedule.name, {
|
|
97
|
+
nextRunAt,
|
|
98
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
99
|
+
});
|
|
100
|
+
state.timeoutId = globalThis.setTimeout(() => {
|
|
101
|
+
void runOnceAndReschedule(state, kernel, invoke, store);
|
|
102
|
+
}, delay);
|
|
103
|
+
};
|
|
104
|
+
const runOnceAndReschedule = async (state, kernel, invoke, store) => {
|
|
105
|
+
const outcome = await invoke(state, kernel);
|
|
106
|
+
scheduleNext(state, kernel, invoke, outcome, store);
|
|
107
|
+
};
|
|
7
108
|
const createRegister = (runner, invokeHandler) => (schedule) => {
|
|
8
|
-
|
|
109
|
+
const existing = runner.schedules.get(schedule.name);
|
|
110
|
+
if (existing !== undefined) {
|
|
9
111
|
Logger.warn(`Schedule replaced: ${schedule.name}`);
|
|
112
|
+
// Reuse the same internal state to avoid leaving old timers/reschedule loops behind.
|
|
113
|
+
existing.schedule = schedule;
|
|
114
|
+
// If disabled, ensure any pending timer is cleared.
|
|
115
|
+
if (schedule.enabled === false) {
|
|
116
|
+
clearTimer(existing);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// If a run is currently in progress, let it finish and reschedule naturally.
|
|
120
|
+
if (existing.isRunning) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// If schedules are already started, apply the new schedule immediately.
|
|
124
|
+
if (runner.started) {
|
|
125
|
+
if (schedule.runOnStart === true) {
|
|
126
|
+
void runOnceAndReschedule(existing, runner.kernel, invokeHandler, runner.store);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
scheduleNext(existing, runner.kernel, invokeHandler, 'success', runner.store);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
10
132
|
}
|
|
11
133
|
const state = {
|
|
12
134
|
schedule,
|
|
13
135
|
isRunning: false,
|
|
136
|
+
consecutiveFailures: 0,
|
|
14
137
|
};
|
|
15
138
|
runner.schedules.set(schedule.name, state);
|
|
16
139
|
// If schedules are already started, register should take effect immediately.
|
|
17
140
|
if (runner.started && schedule.enabled !== false) {
|
|
18
141
|
if (schedule.runOnStart === true) {
|
|
19
|
-
void
|
|
20
|
-
|
|
21
|
-
if (typeof schedule.intervalMs === 'number' && schedule.intervalMs > 0) {
|
|
22
|
-
state.intervalId = globalThis.setInterval(() => {
|
|
23
|
-
void invokeHandler(state, runner.kernel);
|
|
24
|
-
}, schedule.intervalMs);
|
|
142
|
+
void runOnceAndReschedule(state, runner.kernel, invokeHandler, runner.store);
|
|
143
|
+
return;
|
|
25
144
|
}
|
|
145
|
+
// Auto scheduling
|
|
146
|
+
scheduleNext(state, runner.kernel, invokeHandler, 'success', runner.store);
|
|
26
147
|
}
|
|
27
148
|
};
|
|
28
|
-
const createInvokeHandler = () => async (state, kernel) => {
|
|
149
|
+
const createInvokeHandler = (store) => async (state, kernel) => {
|
|
29
150
|
if (state.isRunning) {
|
|
30
151
|
Logger.info(`Skipping overlapping run for schedule: ${state.schedule.name}`);
|
|
31
|
-
return;
|
|
152
|
+
return 'failure';
|
|
32
153
|
}
|
|
33
154
|
state.isRunning = true;
|
|
34
155
|
try {
|
|
35
156
|
const handlerPromise = Promise.resolve()
|
|
36
157
|
.then(async () => state.schedule.handler(kernel))
|
|
37
158
|
.then(() => {
|
|
38
|
-
state.lastRunAt =
|
|
159
|
+
state.lastRunAt = nowMs();
|
|
160
|
+
state.consecutiveFailures = 0;
|
|
161
|
+
void store.set(state.schedule.name, {
|
|
162
|
+
lastRunAt: state.lastRunAt,
|
|
163
|
+
lastSuccessAt: state.lastRunAt,
|
|
164
|
+
lastErrorAt: undefined,
|
|
165
|
+
lastErrorMessage: undefined,
|
|
166
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
167
|
+
});
|
|
168
|
+
return 'success';
|
|
39
169
|
})
|
|
40
170
|
.catch((error) => {
|
|
171
|
+
state.consecutiveFailures = Math.min(1_000_000, state.consecutiveFailures + 1);
|
|
172
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
173
|
+
const at = nowMs();
|
|
174
|
+
void store.set(state.schedule.name, {
|
|
175
|
+
lastRunAt: at,
|
|
176
|
+
lastErrorAt: at,
|
|
177
|
+
lastErrorMessage: errMsg,
|
|
178
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
179
|
+
});
|
|
41
180
|
Logger.error(`Schedule '${state.schedule.name}' failed:`, error);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
state.runningPromise = handlerPromise;
|
|
45
|
-
await handlerPromise;
|
|
181
|
+
return 'failure';
|
|
182
|
+
});
|
|
183
|
+
state.runningPromise = handlerPromise.then(() => undefined);
|
|
184
|
+
return await handlerPromise;
|
|
46
185
|
}
|
|
47
186
|
finally {
|
|
48
187
|
state.isRunning = false;
|
|
@@ -59,16 +198,12 @@ const createStart = (runner, invokeHandler) => (kernel) => {
|
|
|
59
198
|
if (schedule.enabled === false)
|
|
60
199
|
continue;
|
|
61
200
|
if (schedule.runOnStart === true) {
|
|
62
|
-
// fire-and-forget
|
|
63
|
-
void
|
|
64
|
-
|
|
65
|
-
if (typeof schedule.intervalMs === 'number' && schedule.intervalMs > 0) {
|
|
66
|
-
const id = globalThis.setInterval(() => {
|
|
67
|
-
// fire and forget invocation; overlapping runs are protected inside
|
|
68
|
-
void invokeHandler(state, kernel);
|
|
69
|
-
}, schedule.intervalMs);
|
|
70
|
-
state.intervalId = id;
|
|
201
|
+
// fire-and-forget; next scheduling happens after the handler completes
|
|
202
|
+
void runOnceAndReschedule(state, kernel, invokeHandler, runner.store);
|
|
203
|
+
continue;
|
|
71
204
|
}
|
|
205
|
+
// Auto scheduling
|
|
206
|
+
scheduleNext(state, kernel, invokeHandler, 'success', runner.store);
|
|
72
207
|
}
|
|
73
208
|
};
|
|
74
209
|
const createStop = (runner) => async () => {
|
|
@@ -76,12 +211,9 @@ const createStop = (runner) => async () => {
|
|
|
76
211
|
return;
|
|
77
212
|
runner.started = false;
|
|
78
213
|
runner.kernel = undefined;
|
|
79
|
-
// Clear
|
|
214
|
+
// Clear timers
|
|
80
215
|
for (const [, state] of runner.schedules) {
|
|
81
|
-
|
|
82
|
-
globalThis.clearInterval(state.intervalId);
|
|
83
|
-
state.intervalId = undefined;
|
|
84
|
-
}
|
|
216
|
+
clearTimer(state);
|
|
85
217
|
}
|
|
86
218
|
// Await running handlers (runningPromise is guaranteed not to reject)
|
|
87
219
|
const running = [];
|
|
@@ -136,20 +268,25 @@ export const create = () => {
|
|
|
136
268
|
schedules: new Map(),
|
|
137
269
|
started: false,
|
|
138
270
|
kernel: undefined,
|
|
271
|
+
store: InMemoryScheduleStateStore.create(),
|
|
139
272
|
};
|
|
140
|
-
const invokeHandler = createInvokeHandler();
|
|
273
|
+
const invokeHandler = createInvokeHandler(runner.store);
|
|
141
274
|
const register = createRegister(runner, invokeHandler);
|
|
142
275
|
const start = createStart(runner, invokeHandler);
|
|
143
276
|
const stopRaw = createStop(runner);
|
|
144
277
|
const stop = createStopWithTimeout(stopRaw);
|
|
145
278
|
const list = createList(runner);
|
|
146
279
|
const runOnce = createRunOnce(runner, invokeHandler);
|
|
280
|
+
const getState = async (name) => runner.store.get(name);
|
|
281
|
+
const listStates = async () => runner.store.list();
|
|
147
282
|
return Object.freeze({
|
|
148
283
|
register,
|
|
149
284
|
start,
|
|
150
285
|
stop,
|
|
151
286
|
list,
|
|
152
287
|
runOnce,
|
|
288
|
+
getState,
|
|
289
|
+
listStates,
|
|
153
290
|
});
|
|
154
291
|
};
|
|
155
292
|
export default { create };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ScheduleRunState } from './state/ScheduleStateStore';
|
|
2
|
+
import type { ISchedule, IScheduleKernel } from './types';
|
|
3
|
+
export declare const SchedulerRuntime: Readonly<{
|
|
4
|
+
registerMany: (schedules: ReadonlyArray<ISchedule>, source?: "core" | "app") => void;
|
|
5
|
+
start(kernel?: IScheduleKernel): void;
|
|
6
|
+
stop(timeoutMs?: number): Promise<void>;
|
|
7
|
+
list(): ISchedule[];
|
|
8
|
+
listWithState(): Promise<Array<{
|
|
9
|
+
schedule: ISchedule;
|
|
10
|
+
state: ScheduleRunState | null;
|
|
11
|
+
}>>;
|
|
12
|
+
runOnce(name: string, kernel?: IScheduleKernel): Promise<void>;
|
|
13
|
+
}>;
|
|
14
|
+
export default SchedulerRuntime;
|
|
15
|
+
//# sourceMappingURL=SchedulerRuntime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SchedulerRuntime.d.ts","sourceRoot":"","sources":["../../../src/scheduler/SchedulerRuntime.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAmDnE,eAAO,MAAM,gBAAgB;8BAzBhB,aAAa,CAAC,SAAS,CAAC,WAC3B,MAAM,GAAG,KAAK,KACrB,IAAI;mBAyBU,eAAe,GAAG,IAAI;qBAqBd,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAOrC,SAAS,EAAE;qBAGI,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;kBAkB1E,MAAM,WAAW,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;EAGtC,CAAC;AAEjC,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Logger } from '../config/logger.js';
|
|
2
|
+
import { createScheduleRunner } from './index.js';
|
|
3
|
+
import { SchedulerLeader } from './leader/SchedulerLeader.js';
|
|
4
|
+
const state = {
|
|
5
|
+
runner: createScheduleRunner(),
|
|
6
|
+
registered: new Map(),
|
|
7
|
+
leader: SchedulerLeader.create(),
|
|
8
|
+
leaderStarted: false,
|
|
9
|
+
};
|
|
10
|
+
const registerMany = (schedules, source = 'core') => {
|
|
11
|
+
for (const schedule of schedules) {
|
|
12
|
+
if (schedule === undefined || schedule === null || typeof schedule.name !== 'string')
|
|
13
|
+
continue;
|
|
14
|
+
const name = schedule.name.trim();
|
|
15
|
+
if (name.length === 0)
|
|
16
|
+
continue;
|
|
17
|
+
const existing = state.registered.get(name);
|
|
18
|
+
// If app already registered, it always wins.
|
|
19
|
+
if (existing === 'app')
|
|
20
|
+
continue;
|
|
21
|
+
// Prevent repeated registrations from the same source.
|
|
22
|
+
if (existing === source)
|
|
23
|
+
continue;
|
|
24
|
+
if (existing === 'core' && source === 'app') {
|
|
25
|
+
Logger.info('Schedule overridden by app/Schedules', { name });
|
|
26
|
+
}
|
|
27
|
+
state.registered.set(name, source);
|
|
28
|
+
state.runner.register(schedule);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
export const SchedulerRuntime = Object.freeze({
|
|
32
|
+
registerMany,
|
|
33
|
+
start(kernel) {
|
|
34
|
+
// If leader mode is enabled, only the leader instance starts timers.
|
|
35
|
+
if (state.leader.isEnabled()) {
|
|
36
|
+
if (state.leaderStarted)
|
|
37
|
+
return;
|
|
38
|
+
state.leaderStarted = true;
|
|
39
|
+
state.leader.start({
|
|
40
|
+
onBecameLeader: () => {
|
|
41
|
+
state.runner.start(kernel);
|
|
42
|
+
},
|
|
43
|
+
onLostLeadership: () => {
|
|
44
|
+
// Best-effort stop; leadership transitions should not crash the process.
|
|
45
|
+
void state.runner.stop();
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
state.runner.start(kernel);
|
|
51
|
+
},
|
|
52
|
+
async stop(timeoutMs) {
|
|
53
|
+
if (state.leaderStarted) {
|
|
54
|
+
state.leaderStarted = false;
|
|
55
|
+
await state.leader.stop();
|
|
56
|
+
}
|
|
57
|
+
await state.runner.stop(timeoutMs);
|
|
58
|
+
},
|
|
59
|
+
list() {
|
|
60
|
+
return state.runner.list();
|
|
61
|
+
},
|
|
62
|
+
async listWithState() {
|
|
63
|
+
const schedules = state.runner.list();
|
|
64
|
+
const getState = state.runner
|
|
65
|
+
.getState;
|
|
66
|
+
if (typeof getState !== 'function') {
|
|
67
|
+
return schedules.map((schedule) => ({ schedule, state: null }));
|
|
68
|
+
}
|
|
69
|
+
const rows = await Promise.all(schedules.map(async (schedule) => {
|
|
70
|
+
const stateRow = (await getState(schedule.name));
|
|
71
|
+
return { schedule, state: stateRow };
|
|
72
|
+
}));
|
|
73
|
+
return rows;
|
|
74
|
+
},
|
|
75
|
+
async runOnce(name, kernel) {
|
|
76
|
+
await state.runner.runOnce(name, kernel);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
export default SchedulerRuntime;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type CronAllowed = Readonly<{
|
|
2
|
+
any: true;
|
|
3
|
+
}> | Readonly<{
|
|
4
|
+
any: false;
|
|
5
|
+
values: ReadonlySet<number>;
|
|
6
|
+
}>;
|
|
7
|
+
export type CronSpec = Readonly<{
|
|
8
|
+
minute: CronAllowed;
|
|
9
|
+
hour: CronAllowed;
|
|
10
|
+
dayOfMonth: CronAllowed;
|
|
11
|
+
month: CronAllowed;
|
|
12
|
+
dayOfWeek: CronAllowed;
|
|
13
|
+
}>;
|
|
14
|
+
export declare const Cron: Readonly<{
|
|
15
|
+
parse(expr: string): CronSpec;
|
|
16
|
+
nextRunAtMs(nowMs: number, expr: string, timeZone?: string): number;
|
|
17
|
+
}>;
|
|
18
|
+
export default Cron;
|
|
19
|
+
//# sourceMappingURL=Cron.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Cron.d.ts","sourceRoot":"","sources":["../../../../src/scheduler/cron/Cron.ts"],"names":[],"mappings":"AAEA,KAAK,WAAW,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,IAAI,CAAA;CAAE,CAAC,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAAC;AAEnG,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC;IAC9B,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,WAAW,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,SAAS,EAAE,WAAW,CAAC;CACxB,CAAC,CAAC;AA0LH,eAAO,MAAM,IAAI;gBACH,MAAM,GAAG,QAAQ;uBA+BV,MAAM,QAAQ,MAAM,aAAY,MAAM,GAAW,MAAM;EAoB1E,CAAC;AAEH,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const ANY = Object.freeze({ any: true });
|
|
2
|
+
const toInt = (value) => {
|
|
3
|
+
const n = Number.parseInt(value, 10);
|
|
4
|
+
return Number.isFinite(n) ? n : null;
|
|
5
|
+
};
|
|
6
|
+
const clamp = (n, range) => Math.min(range.max, Math.max(range.min, n));
|
|
7
|
+
const expandStep = (step, range, out) => {
|
|
8
|
+
for (let i = range.min; i <= range.max; i += step)
|
|
9
|
+
out.add(i);
|
|
10
|
+
};
|
|
11
|
+
const expandRange = (trimmed, range, out) => {
|
|
12
|
+
const left = trimmed.slice(0, trimmed.indexOf('-'));
|
|
13
|
+
const rest = trimmed.slice(trimmed.indexOf('-') + 1);
|
|
14
|
+
const slashIdx = rest.indexOf('/');
|
|
15
|
+
const right = slashIdx === -1 ? rest : rest.slice(0, slashIdx);
|
|
16
|
+
const stepRaw = slashIdx === -1 ? null : rest.slice(slashIdx + 1);
|
|
17
|
+
const start = toInt(left);
|
|
18
|
+
const end = toInt(right);
|
|
19
|
+
const step = stepRaw === null ? 1 : toInt(stepRaw);
|
|
20
|
+
if (start === null || end === null || step === null || step <= 0)
|
|
21
|
+
return;
|
|
22
|
+
const a = clamp(start, range);
|
|
23
|
+
const b = clamp(end, range);
|
|
24
|
+
const lo = Math.min(a, b);
|
|
25
|
+
const hi = Math.max(a, b);
|
|
26
|
+
for (let i = lo; i <= hi; i += step)
|
|
27
|
+
out.add(i);
|
|
28
|
+
};
|
|
29
|
+
const expandPart = (part, range, out) => {
|
|
30
|
+
const trimmed = part.trim();
|
|
31
|
+
if (trimmed.length === 0)
|
|
32
|
+
return;
|
|
33
|
+
if (trimmed.startsWith('*/')) {
|
|
34
|
+
const step = toInt(trimmed.slice(2));
|
|
35
|
+
if (step !== null && step > 0)
|
|
36
|
+
expandStep(step, range, out);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (trimmed.includes('-')) {
|
|
40
|
+
expandRange(trimmed, range, out);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const num = toInt(trimmed);
|
|
44
|
+
if (num !== null)
|
|
45
|
+
out.add(clamp(num, range));
|
|
46
|
+
};
|
|
47
|
+
const parseField = (raw, range, normalize) => {
|
|
48
|
+
const value = raw.trim();
|
|
49
|
+
if (value === '*' || value.length === 0)
|
|
50
|
+
return ANY;
|
|
51
|
+
const set = new Set();
|
|
52
|
+
for (const part of value.split(',')) {
|
|
53
|
+
expandPart(part, range, set);
|
|
54
|
+
}
|
|
55
|
+
if (normalize !== undefined) {
|
|
56
|
+
const normalized = new Set();
|
|
57
|
+
for (const n of set)
|
|
58
|
+
normalized.add(normalize(n));
|
|
59
|
+
return Object.freeze({ any: false, values: normalized });
|
|
60
|
+
}
|
|
61
|
+
return Object.freeze({ any: false, values: set });
|
|
62
|
+
};
|
|
63
|
+
const matches = (allowed, value) => {
|
|
64
|
+
if (allowed.any)
|
|
65
|
+
return true;
|
|
66
|
+
return allowed.values.has(value);
|
|
67
|
+
};
|
|
68
|
+
const weekdayToDow = (weekdayShort) => {
|
|
69
|
+
switch (weekdayShort) {
|
|
70
|
+
case 'Sun':
|
|
71
|
+
return 0;
|
|
72
|
+
case 'Mon':
|
|
73
|
+
return 1;
|
|
74
|
+
case 'Tue':
|
|
75
|
+
return 2;
|
|
76
|
+
case 'Wed':
|
|
77
|
+
return 3;
|
|
78
|
+
case 'Thu':
|
|
79
|
+
return 4;
|
|
80
|
+
case 'Fri':
|
|
81
|
+
return 5;
|
|
82
|
+
case 'Sat':
|
|
83
|
+
return 6;
|
|
84
|
+
default:
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const dtfCache = new Map();
|
|
89
|
+
const getFormatter = (timeZone) => {
|
|
90
|
+
const key = `en-US|${timeZone}`;
|
|
91
|
+
const cached = dtfCache.get(key);
|
|
92
|
+
if (cached)
|
|
93
|
+
return cached;
|
|
94
|
+
const fmt = new Intl.DateTimeFormat('en-US', {
|
|
95
|
+
timeZone,
|
|
96
|
+
weekday: 'short',
|
|
97
|
+
year: 'numeric',
|
|
98
|
+
month: '2-digit',
|
|
99
|
+
day: '2-digit',
|
|
100
|
+
hour: '2-digit',
|
|
101
|
+
minute: '2-digit',
|
|
102
|
+
hourCycle: 'h23',
|
|
103
|
+
});
|
|
104
|
+
dtfCache.set(key, fmt);
|
|
105
|
+
return fmt;
|
|
106
|
+
};
|
|
107
|
+
const getZonedParts = (date, timeZone) => {
|
|
108
|
+
// Fallback to UTC if Intl timeZone support is unavailable.
|
|
109
|
+
try {
|
|
110
|
+
const fmt = getFormatter(timeZone);
|
|
111
|
+
const parts = fmt.formatToParts(date);
|
|
112
|
+
const get = (type) => parts.find((p) => p.type === type)?.value;
|
|
113
|
+
const minute = Number.parseInt(get('minute') ?? '0', 10);
|
|
114
|
+
const hour = Number.parseInt(get('hour') ?? '0', 10);
|
|
115
|
+
const day = Number.parseInt(get('day') ?? '1', 10);
|
|
116
|
+
const month = Number.parseInt(get('month') ?? '1', 10);
|
|
117
|
+
const weekday = get('weekday') ?? 'Sun';
|
|
118
|
+
const dow = weekdayToDow(weekday);
|
|
119
|
+
return { minute, hour, day, month, dow };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return {
|
|
123
|
+
minute: date.getUTCMinutes(),
|
|
124
|
+
hour: date.getUTCHours(),
|
|
125
|
+
day: date.getUTCDate(),
|
|
126
|
+
month: date.getUTCMonth() + 1,
|
|
127
|
+
dow: date.getUTCDay(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const domDowMatches = (spec, parts) => {
|
|
132
|
+
const domAny = spec.dayOfMonth.any;
|
|
133
|
+
const dowAny = spec.dayOfWeek.any;
|
|
134
|
+
const domOk = matches(spec.dayOfMonth, parts.day);
|
|
135
|
+
const dowOk = matches(spec.dayOfWeek, parts.dow);
|
|
136
|
+
// Vixie cron semantics:
|
|
137
|
+
// - if either DOM or DOW is '*', require the other field to match
|
|
138
|
+
// - if both are restricted, match if either matches
|
|
139
|
+
if (domAny && dowAny)
|
|
140
|
+
return true;
|
|
141
|
+
if (domAny)
|
|
142
|
+
return dowOk;
|
|
143
|
+
if (dowAny)
|
|
144
|
+
return domOk;
|
|
145
|
+
return domOk || dowOk;
|
|
146
|
+
};
|
|
147
|
+
const matchesSpec = (spec, parts) => {
|
|
148
|
+
return (matches(spec.minute, parts.minute) &&
|
|
149
|
+
matches(spec.hour, parts.hour) &&
|
|
150
|
+
matches(spec.month, parts.month) &&
|
|
151
|
+
domDowMatches(spec, parts));
|
|
152
|
+
};
|
|
153
|
+
export const Cron = Object.freeze({
|
|
154
|
+
parse(expr) {
|
|
155
|
+
const raw = String(expr ?? '').trim();
|
|
156
|
+
const parts = raw.split(/\s+/).filter(Boolean);
|
|
157
|
+
if (parts.length !== 5) {
|
|
158
|
+
// Return an "any" spec for invalid inputs; runner will treat it as "every minute".
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
minute: ANY,
|
|
161
|
+
hour: ANY,
|
|
162
|
+
dayOfMonth: ANY,
|
|
163
|
+
month: ANY,
|
|
164
|
+
dayOfWeek: ANY,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const [min, hour, dom, month, dow] = parts;
|
|
168
|
+
const normalizeDow = (n) => {
|
|
169
|
+
// allow 7 as Sunday
|
|
170
|
+
if (n === 7)
|
|
171
|
+
return 0;
|
|
172
|
+
return clamp(n, { min: 0, max: 6 });
|
|
173
|
+
};
|
|
174
|
+
return Object.freeze({
|
|
175
|
+
minute: parseField(min, { min: 0, max: 59 }),
|
|
176
|
+
hour: parseField(hour, { min: 0, max: 23 }),
|
|
177
|
+
dayOfMonth: parseField(dom, { min: 1, max: 31 }),
|
|
178
|
+
month: parseField(month, { min: 1, max: 12 }),
|
|
179
|
+
dayOfWeek: parseField(dow, { min: 0, max: 7 }, normalizeDow),
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
nextRunAtMs(nowMs, expr, timeZone = 'UTC') {
|
|
183
|
+
const spec = this.parse(expr);
|
|
184
|
+
const base = new Date(nowMs);
|
|
185
|
+
// Cron is minute-resolution; start from next minute boundary.
|
|
186
|
+
base.setUTCSeconds(0, 0);
|
|
187
|
+
base.setTime(base.getTime() + 60_000);
|
|
188
|
+
// Bound search to 366 days (minute granularity). This is defensive; typical crons resolve quickly.
|
|
189
|
+
const maxIterations = 366 * 24 * 60;
|
|
190
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
191
|
+
const parts = getZonedParts(base, timeZone);
|
|
192
|
+
if (matchesSpec(spec, parts))
|
|
193
|
+
return base.getTime();
|
|
194
|
+
base.setTime(base.getTime() + 60_000);
|
|
195
|
+
}
|
|
196
|
+
// Fallback: run in 60s.
|
|
197
|
+
return nowMs + 60_000;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
export default Cron;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type SchedulerLeaderHooks = Readonly<{
|
|
2
|
+
onBecameLeader: () => void;
|
|
3
|
+
onLostLeadership: () => void;
|
|
4
|
+
}>;
|
|
5
|
+
export type SchedulerLeaderApi = Readonly<{
|
|
6
|
+
isEnabled: () => boolean;
|
|
7
|
+
start: (hooks: SchedulerLeaderHooks) => void;
|
|
8
|
+
stop: () => Promise<void>;
|
|
9
|
+
}>;
|
|
10
|
+
export declare const SchedulerLeader: Readonly<{
|
|
11
|
+
create(): SchedulerLeaderApi;
|
|
12
|
+
}>;
|
|
13
|
+
export default SchedulerLeader;
|
|
14
|
+
//# sourceMappingURL=SchedulerLeader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SchedulerLeader.d.ts","sourceRoot":"","sources":["../../../../src/scheduler/leader/SchedulerLeader.ts"],"names":[],"mappings":"AAoFA,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,gBAAgB,EAAE,MAAM,IAAI,CAAC;CAC9B,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,KAAK,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC7C,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B,CAAC,CAAC;AAgGH,eAAO,MAAM,eAAe;cAChB,kBAAkB;EAkD5B,CAAC;AAEH,eAAe,eAAe,CAAC"}
|