alepha 0.20.2 → 0.20.4
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/README.md +0 -1
- package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/assets/swagger-ui/swagger-ui.css +1 -1
- package/dist/api/audits/index.browser.js +49 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.js +49 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +2 -61
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +4 -4
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +1 -10
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/parameters/index.browser.js +37 -0
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +12 -68
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +57 -4
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.browser.js +6 -0
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +148 -227
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +60 -14
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js +2 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/bucket/index.d.ts +77 -107
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +153 -5
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +12 -2
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +26 -0
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +11 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +11 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.d.ts +7 -5
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +2 -3
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +637 -11660
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +707 -532
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +4 -8
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js +20 -16
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +51 -77
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +65 -15
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +10 -13
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +30 -12
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.js +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +27 -3
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +8 -11
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +27 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +27 -3
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +27 -3
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.d.ts +69 -10
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +135 -13
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/smtp/index.js +130 -16
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +30 -2
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +35 -12
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.js +32 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +238 -31
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +198 -67
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +2 -362
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +18 -409
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +41 -194
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +27 -422
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +17 -20
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +1 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +17 -20
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/core/index.d.ts +102 -1
- package/dist/react/core/index.d.ts.map +1 -1
- package/dist/react/core/index.js +65 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +6 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +7 -7
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +7 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js +6 -0
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js +22 -17
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js +98 -4
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +58 -5
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +122 -6
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/{chunk-DBEY4PJZ.js → chunk-6Ep1yQYe.js} +1 -1
- package/dist/react/testing/index.js +1 -1
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +195 -1
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/index.js +64 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/react/websocket/index.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +1 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +1 -1
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +2 -2
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +24 -10
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js +10 -3
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +1 -4
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +47 -9
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js +19 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +4 -5
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/websocket/index.browser.js +32 -5
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +3 -1
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +42 -6
- package/dist/websocket/index.js.map +1 -1
- package/package.json +685 -274
- package/src/api/files/__tests__/FileController.spec.ts +1 -1
- package/src/api/jobs/__tests__/$job.spec.ts +5 -1
- package/src/api/parameters/services/ParameterProvider.ts +21 -4
- package/src/api/users/__tests__/SessionService.spec.ts +99 -0
- package/src/api/users/__tests__/UserJobs.spec.ts +67 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +15 -0
- package/src/api/users/entities/sessions.ts +6 -0
- package/src/api/users/jobs/UserJobs.ts +44 -17
- package/src/api/users/providers/RealmProvider.ts +4 -0
- package/src/api/users/schemas/userQuerySchema.ts +0 -1
- package/src/api/users/services/SessionService.ts +27 -0
- package/src/api/users/services/UserService.ts +1 -5
- package/src/api/verifications/__tests__/CodeVerification.spec.ts +14 -0
- package/src/api/verifications/__tests__/LinkVerification.spec.ts +14 -0
- package/src/api/verifications/services/VerificationService.ts +1 -0
- package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +74 -0
- package/src/bucket/index.ts +19 -2
- package/src/bucket/primitives/$bucket.ts +9 -1
- package/src/bucket/providers/CloudflareR2Provider.ts +2 -137
- package/src/bucket/providers/NodeS3BucketProvider.ts +218 -0
- package/src/cache/core/index.ts +29 -0
- package/src/cache/core/primitives/$cache.ts +14 -1
- package/src/cli/config/defineConfig.ts +13 -15
- package/src/cli/core/__tests__/init.spec.ts +214 -7
- package/src/cli/core/commands/init.ts +12 -0
- package/src/cli/core/services/PackageManagerUtils.ts +23 -6
- package/src/cli/core/services/ProjectScaffolder.ts +315 -33
- package/src/cli/core/tasks/BuildCloudflareTask.ts +5 -0
- package/src/cli/core/tasks/BuildDockerTask.ts +9 -10
- package/src/cli/core/tasks/BuildServerTask.ts +8 -0
- package/src/cli/core/templates/agentMd.ts +2 -10
- package/src/cli/core/templates/apiIndexTs.ts +23 -1
- package/src/cli/core/templates/componentsJsonTs.ts +39 -0
- package/src/cli/core/templates/mainCss.ts +1 -0
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +77 -0
- package/src/cli/core/templates/saasAdminPagesTsx.ts +26 -0
- package/src/cli/core/templates/saasAuthLayoutTsx.ts +20 -0
- package/src/cli/core/templates/saasAuthPagesTsx.ts +62 -0
- package/src/cli/core/templates/saasRealmProviderTs.ts +46 -0
- package/src/cli/core/templates/webAppRouterTs.ts +104 -1
- package/src/cli/core/templates/webIndexTs.ts +23 -1
- package/src/cli/devtools/index.ts +12 -26
- package/src/cli/platform/__tests__/SecretsCommand.spec.ts +2 -0
- package/src/cli/platform/index.ts +15 -24
- package/src/cli/vendor/atoms/vendorOptions.ts +1 -1
- package/src/cli/vendor/index.ts +14 -23
- package/src/command/providers/CliProvider.ts +1 -1
- package/src/core/Alepha.ts +11 -1
- package/src/core/helpers/ref.ts +18 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/interfaces/Service.ts +3 -1
- package/src/core/providers/SchemaValidator.ts +9 -1
- package/src/core/providers/TypeProvider.ts +2 -3
- package/src/datetime/REFACTORING.md +118 -0
- package/src/datetime/providers/DateTimeProvider.ts +203 -24
- package/src/lock/core/index.ts +31 -0
- package/src/lock/core/primitives/$lock.ts +14 -1
- package/src/logger/services/Logger.ts +1 -1
- package/src/mcp/__tests__/$resource.spec.ts +1 -1
- package/src/mcp/__tests__/$tool.spec.ts +1 -1
- package/src/mcp/__tests__/McpServerProvider.spec.ts +1 -1
- package/src/mcp/__tests__/jsonrpc.spec.ts +1 -1
- package/src/mcp/helpers/jsonrpc.ts +26 -1
- package/src/mcp/index.ts +10 -5
- package/src/mcp/interfaces/McpTypes.ts +83 -6
- package/src/mcp/primitives/$prompt.ts +18 -1
- package/src/mcp/primitives/$resource.ts +18 -1
- package/src/mcp/primitives/$tool.ts +83 -7
- package/src/mcp/providers/McpServerProvider.ts +74 -16
- package/src/mcp/transports/StreamableHttpMcpTransport.ts +226 -0
- package/src/orm/REFACTORING.md +330 -0
- package/src/orm/__tests__/$repository-tests.ts +1 -0
- package/src/orm/__tests__/orm-next-tests.ts +2 -67
- package/src/orm/__tests__/orm-next.spec.ts +0 -21
- package/src/orm/core/index.shared.ts +0 -2
- package/src/orm/core/index.ts +1 -2
- package/src/orm/core/primitives/$repository.ts +3 -6
- package/src/orm/core/primitives/$transactional.ts +11 -0
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +0 -5
- package/src/orm/core/providers/drivers/NodeSqliteProvider.ts +11 -13
- package/src/orm/core/schemas/updateSchema.ts +1 -1
- package/src/orm/core/services/ModelBuilder.ts +1 -13
- package/src/orm/core/services/PgRelationManager.ts +4 -2
- package/src/orm/core/services/Repository.ts +1 -42
- package/src/orm/core/services/SqliteModelBuilder.ts +2 -33
- package/src/orm/postgres/services/PostgresModelBuilder.ts +10 -45
- package/src/react/core/__tests__/useQuery.browser.spec.tsx +86 -0
- package/src/react/core/hooks/useQuery.ts +153 -0
- package/src/react/core/index.ts +1 -0
- package/src/react/form/services/FormModel.ts +15 -6
- package/src/react/form/services/parseField.ts +8 -0
- package/src/react/i18n/providers/I18nProvider.ts +8 -2
- package/src/react/intro/components/GettingStartedAuthSlide.tsx +11 -4
- package/src/react/router/__tests__/$page.spec.tsx +0 -16
- package/src/react/router/__tests__/ReactBrowserProvider.browser.spec.ts +213 -2
- package/src/react/router/__tests__/ssr.spec.tsx +339 -0
- package/src/react/router/primitives/$page.ts +28 -4
- package/src/react/router/providers/ReactBrowserProvider.ts +73 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +1 -1
- package/src/react/router/providers/ReactPageProvider.ts +27 -9
- package/src/react/router/providers/ReactPreloadProvider.ts +1 -1
- package/src/react/router/providers/ReactServerProvider.ts +1 -0
- package/src/react/ui/atoms/uiThemeListAtom.ts +36 -0
- package/src/react/ui/index.ts +6 -0
- package/src/react/ui/services/SchemaControl.ts +209 -0
- package/src/scheduler/providers/CronProvider.ts +1 -1
- package/src/security/primitives/$basicAuth.ts +1 -1
- package/src/security/primitives/$issuer.ts +6 -3
- package/src/server/auth/providers/ServerAuthProvider.ts +5 -1
- package/src/server/core/__tests__/ServerRouterProvider-serializationError.spec.ts +75 -0
- package/src/server/core/__tests__/ServerRouterProvider-validationError.spec.ts +306 -0
- package/src/server/core/errors/ValidationError.ts +13 -1
- package/src/server/core/interfaces/ServerRequest.ts +1 -0
- package/src/server/core/primitives/$action.ts +16 -5
- package/src/server/core/providers/ServerProvider.ts +1 -1
- package/src/server/core/providers/ServerRouterProvider.ts +28 -6
- package/src/server/core/services/HttpClient.ts +1 -1
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +6 -8
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +10 -4
- package/src/websocket/services/WebSocketClient.ts +11 -5
- package/src/mcp/transports/SseMcpTransport.ts +0 -182
- package/src/orm/core/__tests__/parseQueryString.spec.ts +0 -196
- package/src/orm/core/helpers/parseQueryString.ts +0 -502
- package/src/orm/core/primitives/$view.ts +0 -88
package/src/core/Alepha.ts
CHANGED
|
@@ -827,7 +827,7 @@ export class Alepha {
|
|
|
827
827
|
}
|
|
828
828
|
|
|
829
829
|
/**
|
|
830
|
-
*
|
|
830
|
+
* @alias {@link Alepha#with}.
|
|
831
831
|
*/
|
|
832
832
|
public register<T extends object>(
|
|
833
833
|
serviceEntry: ServiceEntry<T> | { default: ServiceEntry<T> },
|
|
@@ -849,6 +849,16 @@ export class Alepha {
|
|
|
849
849
|
opts.parent !== undefined ? opts.parent : (__alephaRef?.parent ?? Alepha);
|
|
850
850
|
|
|
851
851
|
const transient = lifetime === "transient";
|
|
852
|
+
// TODO: warn-once when scoped lifetime silently falls back to the global
|
|
853
|
+
// singleton registry. This happens when AsyncLocalStorage is not available
|
|
854
|
+
// (typically in the browser, where AlsProvider is a no-op) — the user
|
|
855
|
+
// asked for per-request isolation and got a cross-request singleton.
|
|
856
|
+
// Today this is silent. Plan: detect (this.context.get("registry") === undefined)
|
|
857
|
+
// on a "scoped" inject, log a one-shot warning via the Logger module
|
|
858
|
+
// ("[alepha] scoped DI requested for <Service> but no AsyncLocalStorage
|
|
859
|
+
// context — falling back to singleton. This is expected in browser
|
|
860
|
+
// builds; in server runtimes ensure the request is wrapped in
|
|
861
|
+
// alepha.context.run()."). Gate behind a Set<Service> so we don't spam.
|
|
852
862
|
const registry =
|
|
853
863
|
lifetime === "scoped"
|
|
854
864
|
? (this.context.get<Map<Service, ServiceDefinition>>("registry") ??
|
package/src/core/helpers/ref.ts
CHANGED
|
@@ -54,4 +54,22 @@ export const __alephaRef: {
|
|
|
54
54
|
*
|
|
55
55
|
* One main goal of Alepha is working with classes but without the class verbosity.
|
|
56
56
|
* Forcing to pass Alepha instance in constructors would be a step back in that direction!
|
|
57
|
+
*
|
|
58
|
+
* ---------------------------------------------------------------------------------------------------------------------
|
|
59
|
+
*
|
|
60
|
+
* TODO: harden the cursor against mid-instantiation throws.
|
|
61
|
+
*
|
|
62
|
+
* Today, the cleanup of `__alephaRef.alepha`, `__alephaRef.service`, `__alephaRef.parent`
|
|
63
|
+
* is performed by the caller after the synchronous instantiation returns. If the
|
|
64
|
+
* instantiation throws (e.g., a primitive factory blows up, or a user constructor
|
|
65
|
+
* raises), the cursor can be left holding stale references until the next
|
|
66
|
+
* instantiation overwrites them. In practice this is harmless because Alepha is
|
|
67
|
+
* single-threaded synchronous during boot and the next inject() rewrites the
|
|
68
|
+
* cursor — but it is a sharp edge for debuggers (frame inspection shows ghost
|
|
69
|
+
* state) and any future tooling that introspects __alephaRef out-of-band.
|
|
70
|
+
*
|
|
71
|
+
* Plan: wrap each cursor mutation in Alepha.ts in a try/finally that snapshots
|
|
72
|
+
* the previous value and restores it on exit (even on throw). Equivalent to a
|
|
73
|
+
* lexically-scoped "with" — pushes/pops cleanly. Should be a 5-line refactor in
|
|
74
|
+
* Alepha.ts:1131-1132 once we touch it.
|
|
57
75
|
*/
|
package/src/core/index.shared.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from "./providers/Json.ts";
|
|
|
36
36
|
export * from "./providers/JsonSchemaCodec.ts";
|
|
37
37
|
export * from "./providers/KeylessJsonSchemaCodec.ts";
|
|
38
38
|
export * from "./providers/SchemaCodec.ts";
|
|
39
|
+
export * from "./providers/SchemaValidator.ts";
|
|
39
40
|
export * from "./providers/StateManager.ts";
|
|
40
41
|
export * from "./providers/TypeProvider.ts";
|
|
41
42
|
export * from "./schemas/pageQuerySchema.ts";
|
|
@@ -8,7 +8,9 @@ export type Service<T extends object = any> =
|
|
|
8
8
|
| AbstractClass<T>
|
|
9
9
|
| RunFunction<T>;
|
|
10
10
|
|
|
11
|
-
export type RunFunction<T extends object = any> = (
|
|
11
|
+
export type RunFunction<T extends object = any> = (
|
|
12
|
+
...args: any[]
|
|
13
|
+
) => T | undefined;
|
|
12
14
|
|
|
13
15
|
export type InstantiableClass<T extends object = any> = new (
|
|
14
16
|
...args: any[]
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
2
|
import { Compile, type Validator } from "typebox/compile";
|
|
3
|
+
import * as Value from "typebox/value";
|
|
3
4
|
import { TypeBoxError } from "../errors/TypeBoxError.ts";
|
|
4
5
|
import { $hook } from "../primitives/$hook.ts";
|
|
5
|
-
import { type Static, t
|
|
6
|
+
import { type Static, t } from "./TypeProvider.ts";
|
|
6
7
|
|
|
7
8
|
export class SchemaValidator {
|
|
8
9
|
protected cache = new Map<TSchema, Validator>();
|
|
@@ -38,6 +39,13 @@ export class SchemaValidator {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Deep clone a schema. Useful when a schema is going to be mutated.
|
|
44
|
+
*/
|
|
45
|
+
public clone<T extends TSchema>(schema: T): T {
|
|
46
|
+
return Value.Clone(schema);
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
protected getValidator<T extends TSchema>(schema: T): Validator<{}, T> {
|
|
42
50
|
if (this.cache.has(schema)) {
|
|
43
51
|
return this.cache.get(schema) as Validator<{}, T>;
|
|
@@ -27,12 +27,11 @@ import type {
|
|
|
27
27
|
import { Type } from "typebox";
|
|
28
28
|
import Format from "typebox/format";
|
|
29
29
|
import { Settings } from "typebox/system";
|
|
30
|
-
import * as Value from "typebox/value";
|
|
31
30
|
import { OPTIONS } from "../constants/OPTIONS.ts";
|
|
32
31
|
import type { TypeBoxError } from "../errors/TypeBoxError.ts";
|
|
33
32
|
import { isTypeFile, type TFile, type TStream } from "../helpers/FileLike.ts";
|
|
34
33
|
|
|
35
|
-
export { Format, Type
|
|
34
|
+
export { Format, Type };
|
|
36
35
|
|
|
37
36
|
// https://github.com/sinclairzx81/typebox/blob/main/changelog/1.1.0.md
|
|
38
37
|
Settings.Set({ correctiveParse: true });
|
|
@@ -447,7 +446,7 @@ export class TypeProvider {
|
|
|
447
446
|
/**
|
|
448
447
|
* Create a schema that maps all properties of an object schema to nullable.
|
|
449
448
|
*/
|
|
450
|
-
public nullify =
|
|
449
|
+
public nullify = (schema: TSchema, options?: TObjectOptions): TSchema =>
|
|
451
450
|
Type.Mapped(
|
|
452
451
|
Type.Identifier("K"),
|
|
453
452
|
Type.KeyOf(schema),
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# DateTime — Refactoring Roadmap
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
Step 1 (done) isolated dayjs behind two wrapper classes — `DateTime` and `Duration` — exposed by `DateTimeProvider`. Nothing in the codebase imports dayjs directly anymore; only this module does.
|
|
6
|
+
|
|
7
|
+
Step 2 (this document) is the engine swap: replace the dayjs internals of those wrappers with `Temporal` + `Intl`, then remove dayjs entirely. **Public API stays identical**, so call-sites across the framework do not change.
|
|
8
|
+
|
|
9
|
+
This refactoring is **deferred** until `Temporal` is natively available across our supported runtimes (Node, Bun, modern browsers). Today (2026-05) only Firefox ships it stably; V8/JSC are still flagged. Pulling the trigger before then would force every consumer to ship a ~28 KB polyfill — a regression vs the current dayjs footprint.
|
|
10
|
+
|
|
11
|
+
When the time comes, the migration is contained to **two files** (`DateTimeProvider.ts` and `react/i18n/providers/I18nProvider.ts`) plus `package.json`.
|
|
12
|
+
|
|
13
|
+
## Polyfill policy
|
|
14
|
+
|
|
15
|
+
`alepha` will **not** bundle or depend on a Temporal polyfill. Apps that need to support runtimes without native Temporal install one themselves and load it once at startup:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// app entry point
|
|
19
|
+
import "temporal-polyfill/global";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Rationale:
|
|
23
|
+
- Polyfill choice is an app concern (bundle size, runtime targets).
|
|
24
|
+
- The polyfill is global-only by nature; importing it from a library would force it on every consumer regardless of need.
|
|
25
|
+
- Once native support is universal, the polyfill becomes a no-op and apps drop the import. `alepha` itself never had to change.
|
|
26
|
+
|
|
27
|
+
The framework's documentation should mention the polyfill recommendation in the upgrade notes for the release that performs the swap.
|
|
28
|
+
|
|
29
|
+
## Wrappers — what changes inside
|
|
30
|
+
|
|
31
|
+
### `DateTime`
|
|
32
|
+
|
|
33
|
+
Currently wraps `Dayjs`. After the swap it wraps a `Temporal.Instant` (and an optional timezone string for the `tz()` chain). Per-method translation:
|
|
34
|
+
|
|
35
|
+
| Method | Temporal mapping |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `add(n, unit)` / `add(Duration)` | `instant.add(Temporal.Duration.from({ [unit]: n }))` |
|
|
38
|
+
| `subtract(n, unit)` / `subtract(Duration)` | `instant.subtract(...)` |
|
|
39
|
+
| `startOf(unit)` / `endOf(unit)` | Convert to `ZonedDateTime`, zero out lower fields, convert back. ~10 lines per unit. No native equivalent. |
|
|
40
|
+
| `isAfter` / `isBefore` / `isSame` | `Temporal.Instant.compare(a, b)` |
|
|
41
|
+
| `diff(other, unit?)` | `instant.since(other).total({ unit })` (default unit: `"milliseconds"`) |
|
|
42
|
+
| `tz(zone)` | Store `zone` on the wrapper; convert via `instant.toZonedDateTimeISO(zone)` when needed for formatting/`startOf`. |
|
|
43
|
+
| `locale(lang)` | Store `lang` on the wrapper (consumed only by `format`/`fromNow`). Or drop the chained form and pass `lang` directly. |
|
|
44
|
+
| `format(template?)` | See **Format tokens** below. |
|
|
45
|
+
| `fromNow(withoutSuffix?)` | `Intl.RelativeTimeFormat` — pick the largest non-zero unit from `since(now)`. ~25 lines. Needs `lang` (currently inherited from `.locale()` chain). |
|
|
46
|
+
| `toISOString()` | `instant.toString()` |
|
|
47
|
+
| `toDate()` | `new Date(instant.epochMilliseconds)` |
|
|
48
|
+
| `valueOf()` | `instant.epochMilliseconds` |
|
|
49
|
+
| `unix()` | `Math.floor(instant.epochMilliseconds / 1000)` |
|
|
50
|
+
| `toJSON` / `toString` | same as `toISOString()` |
|
|
51
|
+
| `toDayjs()` | **Removed.** Verify with grep first — currently used only inside `DateTimeProvider`. |
|
|
52
|
+
|
|
53
|
+
### `Duration`
|
|
54
|
+
|
|
55
|
+
Currently wraps `dayjsDuration.Duration`. After the swap it wraps `Temporal.Duration`. Per-method translation:
|
|
56
|
+
|
|
57
|
+
| Method | Temporal mapping |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `asMilliseconds()` etc. | `duration.total({ unit: "milliseconds" })` etc. |
|
|
60
|
+
| `as(unit)` | `duration.total({ unit })` |
|
|
61
|
+
| `toISOString()` | `duration.toString()` (Temporal already emits ISO 8601) |
|
|
62
|
+
| `toDayjs()` | **Removed.** |
|
|
63
|
+
|
|
64
|
+
Constructor input normalization (number, `[n, unit]` tuple, ISO string `"PT5M"`, existing `Duration`) stays in `DateTimeProvider.duration()`. Temporal parses ISO strings natively via `Temporal.Duration.from()`; the tuple form needs a 3-line shim.
|
|
65
|
+
|
|
66
|
+
## Format tokens — the one open decision
|
|
67
|
+
|
|
68
|
+
`DateTime.format(template)` currently delegates to dayjs and supports two flavours:
|
|
69
|
+
|
|
70
|
+
1. **Localized presets** — `"L"`, `"LL"`, `"LLL"`, `"lll"`, etc. (from `dayjs/plugin/localizedFormat`).
|
|
71
|
+
2. **Token strings** — `"YYYY-MM-DD HH:mm:ss"`.
|
|
72
|
+
|
|
73
|
+
`Intl.DateTimeFormat` uses option objects, not tokens. Two paths:
|
|
74
|
+
|
|
75
|
+
**(a) Drop tokens — preferred.** Force callers to use `Intl.DateTimeFormat` options. `I18nProvider.l` already supports this path (`options.date` accepts an `Intl.DateTimeFormatOptions` object). The token-string path becomes the fallback to remove. Audit before removal — current callers of `format(token)`:
|
|
76
|
+
|
|
77
|
+
- `react/i18n/providers/I18nProvider.ts` — `dt.locale(lang).format(options.date)` when `options.date` is a string
|
|
78
|
+
- `scheduler/providers/WorkerdCronProvider.ts` — `now.format()` (no template — equivalent to ISO string)
|
|
79
|
+
|
|
80
|
+
If those are the only callers, deletion is a 5-line change.
|
|
81
|
+
|
|
82
|
+
**(b) Token mapper.** Keep tokens, write a small dayjs-token → `Intl.DateTimeFormat` translator (~80 lines for `YYYY/MM/DD/HH/mm/ss/L/LL/LLL/lll`). Higher maintenance cost, no real ergonomic win. Recommend against unless external consumers depend on the token form.
|
|
83
|
+
|
|
84
|
+
## I18n provider changes
|
|
85
|
+
|
|
86
|
+
`react/i18n/providers/I18nProvider.ts` is the only file outside `datetime/` that exercises the soon-to-change parts of the API:
|
|
87
|
+
|
|
88
|
+
- `dt.tz(timezone)` — unchanged signature, unchanged caller code.
|
|
89
|
+
- `dt.locale(lang).fromNow()` — becomes `dt.fromNow(lang)` (or `relativeFormat(dt, lang)` helper). Caller updated.
|
|
90
|
+
- `dt.locale(lang).format(token)` — covered by the format-token decision above.
|
|
91
|
+
- The `Intl.DateTimeFormat`/`Intl.NumberFormat` paths in this file already work and are unaffected.
|
|
92
|
+
|
|
93
|
+
## Test surface
|
|
94
|
+
|
|
95
|
+
- `datetime/__tests__/DateTimeProvider.spec.ts` — exercises `pause`/`travel`/`createTimeout`/`wait`/`deadline`/`duration`/`isDurationLike`. All operate on epoch-ms internals; should pass unchanged.
|
|
96
|
+
- All ~3400 other tests across the framework hit the wrappers via the public API. They are the safety net.
|
|
97
|
+
|
|
98
|
+
## Removal checklist
|
|
99
|
+
|
|
100
|
+
When Step 2 ships:
|
|
101
|
+
|
|
102
|
+
1. `package.json` — remove `dayjs`. No new dep added (polyfill is app-side).
|
|
103
|
+
2. `DateTimeProvider.ts` — remove all `dayjs/*` imports, the `PLUGINS` static, the `for (plugin of PLUGINS) DayjsApi.extend(plugin)` constructor block, and the `unwrap`/`toDayjs` helpers.
|
|
104
|
+
3. `DateTime.toDayjs()` and `Duration.toDayjs()` — delete.
|
|
105
|
+
4. Re-exported dayjs unit types (`ManipulateType`, `OpUnitType`, `QUnitType`, `DurationUnitType`) — replace with locally-defined union types covering the units we actually use. Keep the names if they're referenced externally; just stop sourcing them from dayjs.
|
|
106
|
+
5. `react/i18n/providers/I18nProvider.ts` — adjust per the I18n section above.
|
|
107
|
+
6. Verify: `grep -r dayjs packages apps` returns nothing.
|
|
108
|
+
7. Run `yarn lint && yarn typecheck && yarn test`.
|
|
109
|
+
|
|
110
|
+
## Why not now
|
|
111
|
+
|
|
112
|
+
- Polyfill weight: temporal-polyfill ~28 KB gz vs current dayjs+plugins+locales ~15–20 KB gz. Net regression until native lands.
|
|
113
|
+
- No functional gain for users today — same behaviour, same wire format (ISO 8601 strings via `t.datetime()`), same testing affordances (`pause`/`travel`).
|
|
114
|
+
- The cost of waiting is zero: Step 1 already gave us the abstraction. The engine swap is mechanical when the time is right.
|
|
115
|
+
|
|
116
|
+
## Estimated effort when triggered
|
|
117
|
+
|
|
118
|
+
~1 focused day. The wrapper rewrite is mechanical, the I18n adjustment is small, and the test suite catches regressions immediately.
|
|
@@ -9,24 +9,189 @@ import { $hook, $inject, Alepha } from "alepha";
|
|
|
9
9
|
import DayjsApi, {
|
|
10
10
|
type Dayjs,
|
|
11
11
|
type ManipulateType,
|
|
12
|
+
type OpUnitType,
|
|
12
13
|
type PluginFunc,
|
|
14
|
+
type QUnitType,
|
|
13
15
|
} from "dayjs";
|
|
14
|
-
import dayjsDuration from "dayjs/plugin/duration.js";
|
|
16
|
+
import dayjsDuration, { type DurationUnitType } from "dayjs/plugin/duration.js";
|
|
15
17
|
import dayjsLocalizedFormat from "dayjs/plugin/localizedFormat.js";
|
|
16
18
|
import dayjsRelativeTime from "dayjs/plugin/relativeTime.js";
|
|
17
19
|
import dayjsTimezone from "dayjs/plugin/timezone.js";
|
|
18
20
|
import dayjsUtc from "dayjs/plugin/utc.js";
|
|
19
21
|
|
|
20
|
-
export type
|
|
21
|
-
|
|
22
|
-
export type
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
export type { DurationUnitType, ManipulateType, OpUnitType, QUnitType };
|
|
23
|
+
|
|
24
|
+
export type DateTimeInput = string | number | Date | DateTime | Dayjs;
|
|
25
|
+
|
|
26
|
+
export type DurationLike = number | Duration | [number, ManipulateType];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Immutable wrapper around the underlying date-time engine.
|
|
30
|
+
*
|
|
31
|
+
* Designed to isolate consumers from the engine in use (currently dayjs).
|
|
32
|
+
* Methods that produce a new value return a new `DateTime` instance.
|
|
33
|
+
*/
|
|
34
|
+
export class DateTime {
|
|
35
|
+
protected readonly inner: Dayjs;
|
|
36
|
+
|
|
37
|
+
constructor(inner: Dayjs) {
|
|
38
|
+
this.inner = inner;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add a duration to this date-time.
|
|
43
|
+
*/
|
|
44
|
+
add(amount: number, unit?: ManipulateType): DateTime;
|
|
45
|
+
add(duration: Duration): DateTime;
|
|
46
|
+
add(amount: number | Duration, unit?: ManipulateType): DateTime {
|
|
47
|
+
if (amount instanceof Duration) {
|
|
48
|
+
return new DateTime(this.inner.add(amount.toDayjs()));
|
|
49
|
+
}
|
|
50
|
+
return new DateTime(this.inner.add(amount, unit));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Subtract a duration from this date-time.
|
|
55
|
+
*/
|
|
56
|
+
subtract(amount: number, unit?: ManipulateType): DateTime;
|
|
57
|
+
subtract(duration: Duration): DateTime;
|
|
58
|
+
subtract(amount: number | Duration, unit?: ManipulateType): DateTime {
|
|
59
|
+
if (amount instanceof Duration) {
|
|
60
|
+
return new DateTime(this.inner.subtract(amount.toDayjs()));
|
|
61
|
+
}
|
|
62
|
+
return new DateTime(this.inner.subtract(amount, unit));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
startOf(unit: OpUnitType): DateTime {
|
|
66
|
+
return new DateTime(this.inner.startOf(unit));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
endOf(unit: OpUnitType): DateTime {
|
|
70
|
+
return new DateTime(this.inner.endOf(unit));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
isAfter(other: DateTimeInput): boolean {
|
|
74
|
+
return this.inner.isAfter(toDayjs(other));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
isBefore(other: DateTimeInput): boolean {
|
|
78
|
+
return this.inner.isBefore(toDayjs(other));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isSame(other: DateTimeInput, unit?: OpUnitType): boolean {
|
|
82
|
+
return this.inner.isSame(toDayjs(other), unit);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
diff(other: DateTimeInput, unit?: QUnitType | OpUnitType): number {
|
|
86
|
+
return this.inner.diff(toDayjs(other), unit);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
tz(timezone: string): DateTime {
|
|
90
|
+
return new DateTime(this.inner.tz(timezone));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
locale(lang: string): DateTime {
|
|
94
|
+
return new DateTime(this.inner.locale(lang));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
format(template?: string): string {
|
|
98
|
+
return this.inner.format(template);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fromNow(withoutSuffix?: boolean): string {
|
|
102
|
+
return this.inner.fromNow(withoutSuffix);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
toISOString(): string {
|
|
106
|
+
return this.inner.toISOString();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
toDate(): Date {
|
|
110
|
+
return this.inner.toDate();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
valueOf(): number {
|
|
114
|
+
return this.inner.valueOf();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
unix(): number {
|
|
118
|
+
return this.inner.unix();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
toJSON(): string {
|
|
122
|
+
return this.inner.toISOString();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
toString(): string {
|
|
126
|
+
return this.inner.toISOString();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Escape hatch for the underlying dayjs instance.
|
|
131
|
+
*
|
|
132
|
+
* Use sparingly — anything calling this becomes coupled to dayjs and
|
|
133
|
+
* will need to migrate when the engine is replaced.
|
|
134
|
+
*/
|
|
135
|
+
toDayjs(): Dayjs {
|
|
136
|
+
return this.inner;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Immutable wrapper around the underlying duration engine.
|
|
142
|
+
*/
|
|
143
|
+
export class Duration {
|
|
144
|
+
protected readonly inner: dayjsDuration.Duration;
|
|
145
|
+
|
|
146
|
+
constructor(inner: dayjsDuration.Duration) {
|
|
147
|
+
this.inner = inner;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
asMilliseconds(): number {
|
|
151
|
+
return this.inner.asMilliseconds();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
asSeconds(): number {
|
|
155
|
+
return this.inner.asSeconds();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
asMinutes(): number {
|
|
159
|
+
return this.inner.asMinutes();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
asHours(): number {
|
|
163
|
+
return this.inner.asHours();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
asDays(): number {
|
|
167
|
+
return this.inner.asDays();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
as(unit: DurationUnitType): number {
|
|
171
|
+
return this.inner.as(unit);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
toISOString(): string {
|
|
175
|
+
return this.inner.toISOString();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Escape hatch for the underlying dayjs duration.
|
|
180
|
+
*/
|
|
181
|
+
toDayjs(): dayjsDuration.Duration {
|
|
182
|
+
return this.inner;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
26
185
|
|
|
27
|
-
export const dayjs = DayjsApi;
|
|
28
186
|
export const isDateTime = (value: unknown): value is DateTime => {
|
|
29
|
-
return
|
|
187
|
+
return value instanceof DateTime;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const toDayjs = (value: DateTimeInput): Dayjs => {
|
|
191
|
+
if (value instanceof DateTime) {
|
|
192
|
+
return value.toDayjs();
|
|
193
|
+
}
|
|
194
|
+
return DayjsApi(value as any);
|
|
30
195
|
};
|
|
31
196
|
|
|
32
197
|
export class DateTimeProvider {
|
|
@@ -45,7 +210,7 @@ export class DateTimeProvider {
|
|
|
45
210
|
|
|
46
211
|
constructor() {
|
|
47
212
|
for (const plugin of DateTimeProvider.PLUGINS) {
|
|
48
|
-
|
|
213
|
+
DayjsApi.extend(plugin);
|
|
49
214
|
}
|
|
50
215
|
}
|
|
51
216
|
|
|
@@ -81,33 +246,34 @@ export class DateTimeProvider {
|
|
|
81
246
|
});
|
|
82
247
|
|
|
83
248
|
public setLocale(locale: string): void {
|
|
84
|
-
|
|
249
|
+
DayjsApi.locale(locale);
|
|
85
250
|
}
|
|
86
251
|
|
|
87
252
|
public isDateTime(value: unknown): value is DateTime {
|
|
88
|
-
return
|
|
253
|
+
return value instanceof DateTime;
|
|
89
254
|
}
|
|
90
255
|
|
|
91
256
|
/**
|
|
92
257
|
* Create a new UTC DateTime instance.
|
|
93
258
|
*/
|
|
94
|
-
public utc(
|
|
95
|
-
|
|
96
|
-
): DateTime {
|
|
97
|
-
return dayjs.utc(date);
|
|
259
|
+
public utc(date: DateTimeInput | null | undefined): DateTime {
|
|
260
|
+
return new DateTime(DayjsApi.utc(unwrap(date)));
|
|
98
261
|
}
|
|
99
262
|
|
|
100
263
|
/**
|
|
101
264
|
* Create a new DateTime instance.
|
|
102
265
|
*/
|
|
103
|
-
public of(date:
|
|
104
|
-
|
|
266
|
+
public of(date: DateTimeInput | null | undefined): DateTime {
|
|
267
|
+
if (date instanceof DateTime) {
|
|
268
|
+
return date;
|
|
269
|
+
}
|
|
270
|
+
return new DateTime(DayjsApi(date as any));
|
|
105
271
|
}
|
|
106
272
|
|
|
107
273
|
/**
|
|
108
274
|
* Get the current date as a string.
|
|
109
275
|
*/
|
|
110
|
-
public toISOString(date:
|
|
276
|
+
public toISOString(date: DateTimeInput = this.now()): string {
|
|
111
277
|
return this.of(date).toISOString();
|
|
112
278
|
}
|
|
113
279
|
|
|
@@ -152,7 +318,7 @@ export class DateTimeProvider {
|
|
|
152
318
|
return this.ref;
|
|
153
319
|
}
|
|
154
320
|
|
|
155
|
-
return
|
|
321
|
+
return new DateTime(DayjsApi());
|
|
156
322
|
}
|
|
157
323
|
|
|
158
324
|
/**
|
|
@@ -162,12 +328,16 @@ export class DateTimeProvider {
|
|
|
162
328
|
duration: DurationLike,
|
|
163
329
|
unit?: ManipulateType,
|
|
164
330
|
): Duration => {
|
|
331
|
+
if (duration instanceof Duration) {
|
|
332
|
+
return duration;
|
|
333
|
+
}
|
|
334
|
+
|
|
165
335
|
if (Array.isArray(duration)) {
|
|
166
|
-
return
|
|
336
|
+
return new Duration(DayjsApi.duration(duration[0], duration[1]));
|
|
167
337
|
}
|
|
168
338
|
|
|
169
339
|
if (typeof duration === "number") {
|
|
170
|
-
return
|
|
340
|
+
return new Duration(DayjsApi.duration(duration, unit || "milliseconds"));
|
|
171
341
|
}
|
|
172
342
|
|
|
173
343
|
return duration;
|
|
@@ -175,7 +345,9 @@ export class DateTimeProvider {
|
|
|
175
345
|
|
|
176
346
|
public isDurationLike(value: unknown): value is DurationLike {
|
|
177
347
|
try {
|
|
178
|
-
return
|
|
348
|
+
return DayjsApi.isDuration(
|
|
349
|
+
this.duration(value as DurationLike).toDayjs(),
|
|
350
|
+
);
|
|
179
351
|
} catch {
|
|
180
352
|
return false;
|
|
181
353
|
}
|
|
@@ -261,7 +433,7 @@ export class DateTimeProvider {
|
|
|
261
433
|
): Timeout {
|
|
262
434
|
if (this.ref && now) {
|
|
263
435
|
const next = this.of(now).add(this.duration(duration));
|
|
264
|
-
if (next < this.now()) {
|
|
436
|
+
if (next.valueOf() < this.now().valueOf()) {
|
|
265
437
|
callback();
|
|
266
438
|
}
|
|
267
439
|
return {
|
|
@@ -403,6 +575,13 @@ export class DateTimeProvider {
|
|
|
403
575
|
}
|
|
404
576
|
}
|
|
405
577
|
|
|
578
|
+
const unwrap = (value: DateTimeInput | null | undefined): any => {
|
|
579
|
+
if (value instanceof DateTime) {
|
|
580
|
+
return value.toDayjs();
|
|
581
|
+
}
|
|
582
|
+
return value;
|
|
583
|
+
};
|
|
584
|
+
|
|
406
585
|
export interface Interval {
|
|
407
586
|
timer?: any;
|
|
408
587
|
duration: number;
|
package/src/lock/core/index.ts
CHANGED
|
@@ -13,6 +13,37 @@ export * from "./providers/MemoryLockProvider.ts";
|
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
15
15
|
|
|
16
|
+
declare module "alepha" {
|
|
17
|
+
interface Hooks {
|
|
18
|
+
/**
|
|
19
|
+
* Fires when a lock is successfully acquired.
|
|
20
|
+
*/
|
|
21
|
+
"lock:acquired": {
|
|
22
|
+
name: string;
|
|
23
|
+
id: string;
|
|
24
|
+
maxDurationMs: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Fires when a lock is released (handler completed or threw).
|
|
28
|
+
*/
|
|
29
|
+
"lock:released": {
|
|
30
|
+
name: string;
|
|
31
|
+
id: string;
|
|
32
|
+
heldMs: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Fires when a lock acquisition contends with another holder.
|
|
36
|
+
* Emitted whether the caller eventually acquires (`wait: true`) or fails.
|
|
37
|
+
*/
|
|
38
|
+
"lock:contended": {
|
|
39
|
+
name: string;
|
|
40
|
+
id: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
46
|
+
|
|
16
47
|
/**
|
|
17
48
|
* Resource locking for distributed systems.
|
|
18
49
|
*
|
|
@@ -43,7 +43,7 @@ export const $lock = (options: LockMiddlewareOptions): Middleware => {
|
|
|
43
43
|
return createMiddleware({
|
|
44
44
|
name: "$lock",
|
|
45
45
|
options: options as unknown as Record<string, unknown>,
|
|
46
|
-
handler: ({ next }) => {
|
|
46
|
+
handler: ({ alepha, next }) => {
|
|
47
47
|
const id = crypto.randomUUID();
|
|
48
48
|
const maxDurationMs = dateTimeProvider
|
|
49
49
|
.duration(options.maxDuration ?? [5, "minutes"])
|
|
@@ -71,11 +71,13 @@ export const $lock = (options: LockMiddlewareOptions): Middleware => {
|
|
|
71
71
|
|
|
72
72
|
// Lock already ended (grace period active)
|
|
73
73
|
if (endedAtStr) {
|
|
74
|
+
await alepha.events.emit("lock:contended", { name, id });
|
|
74
75
|
throw new LockAcquireError(name);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// Lock held by someone else
|
|
78
79
|
if (lockId !== id) {
|
|
80
|
+
await alepha.events.emit("lock:contended", { name, id });
|
|
79
81
|
if (options.wait) {
|
|
80
82
|
// Poll until lock is released
|
|
81
83
|
const start = dateTimeProvider.nowMillis();
|
|
@@ -109,10 +111,21 @@ export const $lock = (options: LockMiddlewareOptions): Middleware => {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// We hold the lock — execute handler
|
|
114
|
+
const acquiredAt = dateTimeProvider.nowMillis();
|
|
115
|
+
await alepha.events.emit("lock:acquired", {
|
|
116
|
+
name,
|
|
117
|
+
id,
|
|
118
|
+
maxDurationMs,
|
|
119
|
+
});
|
|
112
120
|
try {
|
|
113
121
|
return await next(...args);
|
|
114
122
|
} finally {
|
|
115
123
|
await lockProvider.del(name);
|
|
124
|
+
await alepha.events.emit("lock:released", {
|
|
125
|
+
name,
|
|
126
|
+
id,
|
|
127
|
+
heldMs: dateTimeProvider.nowMillis() - acquiredAt,
|
|
128
|
+
});
|
|
116
129
|
}
|
|
117
130
|
};
|
|
118
131
|
},
|
|
@@ -146,7 +146,7 @@ export class Logger implements LoggerInterface {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
let _data: object | Error | undefined;
|
|
149
|
-
if (typeof data === "object" &&
|
|
149
|
+
if (typeof data === "object" && data) {
|
|
150
150
|
_data = data;
|
|
151
151
|
} else if (typeof message === "object" && message) {
|
|
152
152
|
_data = message;
|
|
@@ -364,7 +364,7 @@ describe("$resource primitive", () => {
|
|
|
364
364
|
uri: "protected://resource",
|
|
365
365
|
handler: async ({ context }) => {
|
|
366
366
|
const authHeader = context?.headers?.authorization;
|
|
367
|
-
if (!authHeader
|
|
367
|
+
if (!authHeader?.toString().startsWith("Bearer ")) {
|
|
368
368
|
throw new Error("Unauthorized");
|
|
369
369
|
}
|
|
370
370
|
return { text: "Secret content" };
|
|
@@ -379,7 +379,7 @@ describe("$tool primitive", () => {
|
|
|
379
379
|
},
|
|
380
380
|
handler: async ({ context }) => {
|
|
381
381
|
const authHeader = context?.headers?.authorization;
|
|
382
|
-
if (!authHeader
|
|
382
|
+
if (!authHeader?.toString().startsWith("Bearer ")) {
|
|
383
383
|
throw new Error("Unauthorized");
|
|
384
384
|
}
|
|
385
385
|
return "Access granted";
|
|
@@ -751,7 +751,7 @@ describe("McpServerProvider", () => {
|
|
|
751
751
|
schema: { result: t.text() },
|
|
752
752
|
handler: async ({ context }) => {
|
|
753
753
|
const auth = context?.headers?.authorization;
|
|
754
|
-
if (!auth
|
|
754
|
+
if (!auth?.toString().startsWith("Bearer ")) {
|
|
755
755
|
throw new Error("Unauthorized");
|
|
756
756
|
}
|
|
757
757
|
return "Access granted";
|