alepha 0.13.8 → 0.14.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.
- package/dist/api/audits/index.d.ts +418 -338
- package/dist/api/audits/index.d.ts.map +1 -0
- package/dist/api/files/index.d.ts +81 -1
- package/dist/api/files/index.d.ts.map +1 -0
- package/dist/api/jobs/index.d.ts +107 -27
- package/dist/api/jobs/index.d.ts.map +1 -0
- package/dist/api/notifications/index.d.ts +21 -1
- package/dist/api/notifications/index.d.ts.map +1 -0
- package/dist/api/parameters/index.d.ts +455 -8
- package/dist/api/parameters/index.d.ts.map +1 -0
- package/dist/api/users/index.d.ts +844 -840
- package/dist/api/users/index.d.ts.map +1 -0
- package/dist/api/verifications/index.d.ts.map +1 -0
- package/dist/batch/index.d.ts.map +1 -0
- package/dist/bucket/index.d.ts.map +1 -0
- package/dist/cache/core/index.d.ts.map +1 -0
- package/dist/cache/redis/index.d.ts.map +1 -0
- package/dist/cli/index.d.ts +254 -59
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +499 -127
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +217 -10
- package/dist/command/index.d.ts.map +1 -0
- package/dist/command/index.js +350 -74
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +1334 -1318
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +76 -72
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +1337 -1321
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1337 -1321
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts.map +1 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/fake/index.d.ts.map +1 -0
- package/dist/file/index.d.ts.map +1 -0
- package/dist/file/index.js.map +1 -1
- package/dist/lock/core/index.d.ts.map +1 -0
- package/dist/lock/redis/index.d.ts.map +1 -0
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +820 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +978 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/orm/index.d.ts +234 -107
- package/dist/orm/index.d.ts.map +1 -0
- package/dist/orm/index.js +376 -316
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +4 -4
- package/dist/queue/core/index.d.ts.map +1 -0
- package/dist/queue/redis/index.d.ts.map +1 -0
- package/dist/queue/redis/index.js +2 -4
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/redis/index.d.ts +400 -29
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +412 -21
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts.map +1 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/scheduler/index.d.ts +6 -6
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/security/index.d.ts +28 -28
- package/dist/security/index.d.ts.map +1 -0
- package/dist/server/auth/index.d.ts +155 -155
- package/dist/server/auth/index.d.ts.map +1 -0
- package/dist/server/cache/index.d.ts.map +1 -0
- package/dist/server/compress/index.d.ts.map +1 -0
- package/dist/server/cookies/index.d.ts.map +1 -0
- package/dist/server/core/index.d.ts +0 -1
- package/dist/server/core/index.d.ts.map +1 -0
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts.map +1 -0
- package/dist/server/health/index.d.ts +17 -17
- package/dist/server/health/index.d.ts.map +1 -0
- package/dist/server/helmet/index.d.ts +4 -1
- package/dist/server/helmet/index.d.ts.map +1 -0
- package/dist/server/links/index.d.ts +33 -33
- package/dist/server/links/index.d.ts.map +1 -0
- package/dist/server/metrics/index.d.ts.map +1 -0
- package/dist/server/multipart/index.d.ts.map +1 -0
- package/dist/server/multipart/index.js.map +1 -1
- package/dist/server/proxy/index.d.ts.map +1 -0
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts.map +1 -0
- package/dist/server/security/index.d.ts +9 -9
- package/dist/server/security/index.d.ts.map +1 -0
- package/dist/server/static/index.d.ts.map +1 -0
- package/dist/server/swagger/index.d.ts.map +1 -0
- package/dist/sms/index.d.ts.map +1 -0
- package/dist/thread/index.d.ts.map +1 -0
- package/dist/topic/core/index.d.ts.map +1 -0
- package/dist/topic/redis/index.d.ts.map +1 -0
- package/dist/topic/redis/index.js +3 -3
- package/dist/topic/redis/index.js.map +1 -1
- package/dist/vite/index.d.ts +10 -2
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +45 -20
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts.map +1 -0
- package/package.json +9 -4
- package/src/cli/apps/AlephaCli.ts +10 -3
- package/src/cli/apps/AlephaPackageBuilderCli.ts +15 -8
- package/src/cli/assets/mainTs.ts +9 -10
- package/src/cli/atoms/changelogOptions.ts +45 -0
- package/src/cli/commands/ChangelogCommands.ts +259 -0
- package/src/cli/commands/DeployCommands.ts +118 -0
- package/src/cli/commands/DrizzleCommands.ts +230 -10
- package/src/cli/commands/ViteCommands.ts +47 -23
- package/src/cli/defineConfig.ts +15 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/services/AlephaCliUtils.ts +10 -154
- package/src/cli/services/GitMessageParser.ts +77 -0
- package/src/command/helpers/EnvUtils.ts +37 -0
- package/src/command/index.ts +3 -1
- package/src/command/primitives/$command.ts +172 -6
- package/src/command/providers/CliProvider.ts +499 -95
- package/src/core/Alepha.ts +1 -1
- package/src/core/providers/SchemaValidator.ts +23 -1
- package/src/file/providers/NodeFileSystemProvider.ts +3 -1
- package/src/mcp/errors/McpError.ts +72 -0
- package/src/mcp/helpers/jsonrpc.ts +163 -0
- package/src/mcp/index.ts +132 -0
- package/src/mcp/interfaces/McpTypes.ts +248 -0
- package/src/mcp/primitives/$prompt.ts +188 -0
- package/src/mcp/primitives/$resource.ts +171 -0
- package/src/mcp/primitives/$tool.ts +285 -0
- package/src/mcp/providers/McpServerProvider.ts +382 -0
- package/src/mcp/transports/SseMcpTransport.ts +172 -0
- package/src/mcp/transports/StdioMcpTransport.ts +126 -0
- package/src/orm/index.ts +20 -4
- package/src/orm/interfaces/PgQueryWhere.ts +1 -26
- package/src/orm/providers/drivers/BunPostgresProvider.ts +225 -0
- package/src/orm/providers/drivers/BunSqliteProvider.ts +180 -0
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +164 -0
- package/src/orm/providers/drivers/DatabaseProvider.ts +25 -0
- package/src/orm/providers/drivers/NodePostgresProvider.ts +0 -25
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +3 -1
- package/src/orm/services/QueryManager.ts +10 -125
- package/src/queue/redis/providers/RedisQueueProvider.ts +2 -7
- package/src/redis/index.ts +65 -3
- package/src/redis/providers/BunRedisProvider.ts +304 -0
- package/src/redis/providers/BunRedisSubscriberProvider.ts +94 -0
- package/src/redis/providers/NodeRedisProvider.ts +280 -0
- package/src/redis/providers/NodeRedisSubscriberProvider.ts +94 -0
- package/src/redis/providers/RedisProvider.ts +134 -140
- package/src/redis/providers/RedisSubscriberProvider.ts +58 -49
- package/src/server/core/providers/BunHttpServerProvider.ts +0 -3
- package/src/server/core/providers/ServerBodyParserProvider.ts +3 -1
- package/src/server/core/providers/ServerProvider.ts +7 -4
- package/src/server/multipart/providers/ServerMultipartProvider.ts +3 -1
- package/src/server/proxy/providers/ServerProxyProvider.ts +1 -1
- package/src/topic/redis/providers/RedisTopicProvider.ts +3 -3
- package/src/vite/plugins/viteAlephaBuild.ts +8 -2
- package/src/vite/plugins/viteAlephaDev.ts +6 -2
- package/src/vite/tasks/buildServer.ts +2 -1
- package/src/vite/tasks/generateCloudflare.ts +43 -15
- package/src/vite/tasks/runAlepha.ts +1 -0
- package/src/orm/services/PgJsonQueryManager.ts +0 -511
package/dist/core/index.js
CHANGED
|
@@ -421,1483 +421,1556 @@ var TypeBoxError = class extends AlephaError {
|
|
|
421
421
|
};
|
|
422
422
|
|
|
423
423
|
//#endregion
|
|
424
|
-
//#region ../../src/core/
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
424
|
+
//#region ../../src/core/primitives/$hook.ts
|
|
425
|
+
/**
|
|
426
|
+
* Registers a new hook.
|
|
427
|
+
*
|
|
428
|
+
* ```ts
|
|
429
|
+
* import { $hook } from "alepha";
|
|
430
|
+
*
|
|
431
|
+
* class MyProvider {
|
|
432
|
+
* onStart = $hook({
|
|
433
|
+
* name: "start", // or "configure", "ready", "stop", ...
|
|
434
|
+
* handler: async (app) => {
|
|
435
|
+
* // await db.connect(); ...
|
|
436
|
+
* }
|
|
437
|
+
* });
|
|
438
|
+
* }
|
|
439
|
+
* ```
|
|
440
|
+
*
|
|
441
|
+
* Hooks are used to run async functions from all registered providers/services.
|
|
442
|
+
*
|
|
443
|
+
* You can't register a hook after the App has started.
|
|
444
|
+
*
|
|
445
|
+
* It's used under the hood by the `configure`, `start`, and `stop` methods.
|
|
446
|
+
* Some modules also use hooks to run their own logic. (e.g. `alepha/server`).
|
|
447
|
+
*
|
|
448
|
+
* You can create your own hooks by using module augmentation:
|
|
449
|
+
*
|
|
450
|
+
* ```ts
|
|
451
|
+
* declare module "alepha" {
|
|
452
|
+
*
|
|
453
|
+
* interface Hooks {
|
|
454
|
+
* "my:custom:hook": {
|
|
455
|
+
* arg1: string;
|
|
456
|
+
* }
|
|
457
|
+
* }
|
|
458
|
+
* }
|
|
459
|
+
*
|
|
460
|
+
* await alepha.events.emit("my:custom:hook", { arg1: "value" });
|
|
461
|
+
* ```
|
|
462
|
+
*
|
|
463
|
+
*/
|
|
464
|
+
const $hook = (options) => createPrimitive(HookPrimitive, options);
|
|
465
|
+
var HookPrimitive = class extends Primitive {
|
|
466
|
+
called = 0;
|
|
467
|
+
onInit() {
|
|
468
|
+
this.alepha.events.on(this.options.on, {
|
|
469
|
+
caller: this.config.service,
|
|
470
|
+
priority: this.options.priority,
|
|
471
|
+
callback: async (args) => {
|
|
472
|
+
this.called += 1;
|
|
473
|
+
await this.options.handler(args);
|
|
474
|
+
}
|
|
437
475
|
});
|
|
438
|
-
try {
|
|
439
|
-
return this.getValidator(schema).Parse(newValue);
|
|
440
|
-
} catch (error) {
|
|
441
|
-
if (error.cause?.errors?.[0]) throw new TypeBoxError(error.cause.errors[0]);
|
|
442
|
-
throw error;
|
|
443
|
-
}
|
|
444
476
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
477
|
+
};
|
|
478
|
+
$hook[KIND] = HookPrimitive;
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region ../../src/core/helpers/FileLike.ts
|
|
482
|
+
const isTypeFile = (value) => {
|
|
483
|
+
return value && typeof value === "object" && "format" in value && value.format === "binary";
|
|
484
|
+
};
|
|
485
|
+
const isFileLike = (value) => {
|
|
486
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && typeof value.name === "string" && typeof value.type === "string" && typeof value.size === "number" && typeof value.stream.bind(value) === "function";
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region ../../src/core/providers/TypeProvider.ts
|
|
491
|
+
const isUUID = Format.IsUuid;
|
|
492
|
+
var TypeGuard = class {
|
|
493
|
+
isBigInt = (value) => Type.IsString(value) && "format" in value && value.format === "bigint";
|
|
494
|
+
isUUID = (value) => Type.IsString(value) && "format" in value && value.format === "uuid";
|
|
495
|
+
isObject = Type.IsObject;
|
|
496
|
+
isNumber = Type.IsNumber;
|
|
497
|
+
isString = Type.IsString;
|
|
498
|
+
isBoolean = Type.IsBoolean;
|
|
499
|
+
isAny = Type.IsAny;
|
|
500
|
+
isArray = Type.IsArray;
|
|
501
|
+
isOptional = Type.IsOptional;
|
|
502
|
+
isUnion = Type.IsUnion;
|
|
503
|
+
isInteger = Type.IsInteger;
|
|
504
|
+
isNull = Type.IsNull;
|
|
505
|
+
isUndefined = Type.IsUndefined;
|
|
506
|
+
isUnsafe = Type.IsUnsafe;
|
|
507
|
+
isRecord = Type.IsRecord;
|
|
508
|
+
isTuple = Type.IsTuple;
|
|
509
|
+
isVoid = Type.IsVoid;
|
|
510
|
+
isLiteral = Type.IsLiteral;
|
|
511
|
+
isSchema = Type.IsSchema;
|
|
512
|
+
isFile = isTypeFile;
|
|
513
|
+
isDateTime = (schema) => {
|
|
514
|
+
return t.schema.isString(schema) && schema.format === "date-time";
|
|
515
|
+
};
|
|
516
|
+
isDate = (schema) => {
|
|
517
|
+
return t.schema.isString(schema) && schema.format === "date";
|
|
518
|
+
};
|
|
519
|
+
isTime = (schema) => {
|
|
520
|
+
return t.schema.isString(schema) && schema.format === "time";
|
|
521
|
+
};
|
|
522
|
+
isDuration = (schema) => {
|
|
523
|
+
return t.schema.isString(schema) && schema.format === "duration";
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
var TypeProvider = class TypeProvider {
|
|
527
|
+
static format = Format;
|
|
528
|
+
static {
|
|
529
|
+
Format.Set("bigint", (value) => TypeProvider.isValidBigInt(value));
|
|
450
530
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
*/
|
|
457
|
-
beforeParse(schema, value, options) {
|
|
458
|
-
if (!schema) return value;
|
|
459
|
-
if (value === null && !this.isSchemaNullable(schema) && options.nullToUndefined) return;
|
|
460
|
-
if (Array.isArray(value)) return value.map((it) => this.beforeParse(schema.items, it, options));
|
|
461
|
-
if (typeof value === "string" && schema.type === "string") {
|
|
462
|
-
let str = value;
|
|
463
|
-
if (options.trim && schema["~options"]?.trim) str = str.trim();
|
|
464
|
-
if (schema["~options"]?.lowercase) str = str.toLowerCase();
|
|
465
|
-
return str;
|
|
531
|
+
static translateError(error, locale) {
|
|
532
|
+
if (!locale) return error.cause.message;
|
|
533
|
+
for (const [key, value] of Object.entries(Locale)) {
|
|
534
|
+
if (key === "Set" || key === "Get" || key === "Reset") continue;
|
|
535
|
+
if (key === locale || key.startsWith(`${locale}_`)) return value(error.cause);
|
|
466
536
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
537
|
+
return error.cause.message;
|
|
538
|
+
}
|
|
539
|
+
static setLocale(locale) {
|
|
540
|
+
for (const [key, value] of Object.entries(Locale)) {
|
|
541
|
+
if (key === "Set" || key === "Get" || key === "Reset") continue;
|
|
542
|
+
if (key === locale || key.startsWith(`${locale}_`)) {
|
|
543
|
+
Locale.Set(value);
|
|
544
|
+
return;
|
|
473
545
|
}
|
|
474
|
-
return obj;
|
|
475
546
|
}
|
|
476
|
-
|
|
547
|
+
throw new AlephaError(`Locale not found: ${locale}`);
|
|
477
548
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return false;
|
|
489
|
-
};
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
//#endregion
|
|
493
|
-
//#region ../../src/core/providers/CodecManager.ts
|
|
494
|
-
/**
|
|
495
|
-
* CodecManager manages multiple codec formats and provides a unified interface
|
|
496
|
-
* for encoding and decoding data with different formats.
|
|
497
|
-
*/
|
|
498
|
-
var CodecManager = class {
|
|
499
|
-
codecs = /* @__PURE__ */ new Map();
|
|
500
|
-
jsonCodec = $inject(JsonSchemaCodec);
|
|
501
|
-
schemaValidator = $inject(SchemaValidator);
|
|
502
|
-
default = "json";
|
|
503
|
-
constructor() {
|
|
504
|
-
this.register(this.default, this.jsonCodec);
|
|
549
|
+
static isValidBigInt(value) {
|
|
550
|
+
if (typeof value === "number") return Number.isInteger(value);
|
|
551
|
+
if (!value.trim()) return false;
|
|
552
|
+
if (!/^-?\d+$/.test(value)) return false;
|
|
553
|
+
try {
|
|
554
|
+
BigInt(value);
|
|
555
|
+
return true;
|
|
556
|
+
} catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
505
559
|
}
|
|
506
560
|
/**
|
|
507
|
-
*
|
|
561
|
+
* Default maximum length for strings.
|
|
508
562
|
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
563
|
+
* It can be set to a larger value:
|
|
564
|
+
* ```ts
|
|
565
|
+
* TypeProvider.DEFAULT_STRING_MAX_LENGTH = 1000000;
|
|
566
|
+
* TypeProvider.DEFAULT_STRING_MAX_LENGTH = undefined; // no limit (not recommended)
|
|
567
|
+
* ```
|
|
511
568
|
*/
|
|
512
|
-
|
|
513
|
-
this.codecs.set(name, codec);
|
|
514
|
-
}
|
|
569
|
+
static DEFAULT_STRING_MAX_LENGTH = 255;
|
|
515
570
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
* @param name - The name of the codec
|
|
519
|
-
* @returns The codec instance
|
|
520
|
-
* @throws {AlephaError} If the codec is not found
|
|
571
|
+
* Maximum length for short strings, such as names or titles.
|
|
521
572
|
*/
|
|
522
|
-
|
|
523
|
-
const codec = this.codecs.get(name);
|
|
524
|
-
if (!codec) throw new AlephaError(`Codec "${name}" not found. Available codecs: ${Array.from(this.codecs.keys()).join(", ")}`);
|
|
525
|
-
return codec;
|
|
526
|
-
}
|
|
573
|
+
static DEFAULT_SHORT_STRING_MAX_LENGTH = 64;
|
|
527
574
|
/**
|
|
528
|
-
*
|
|
575
|
+
* Maximum length for long strings, such as descriptions or comments.
|
|
576
|
+
* It can be overridden in the string options.
|
|
577
|
+
*
|
|
578
|
+
* It can be set to a larger value:
|
|
579
|
+
* ```ts
|
|
580
|
+
* TypeProvider.DEFAULT_LONG_STRING_MAX_LENGTH = 2048;
|
|
581
|
+
* ```
|
|
529
582
|
*/
|
|
530
|
-
|
|
531
|
-
const codec = this.getCodec(options?.encoder ?? this.default);
|
|
532
|
-
const as = options?.as ?? "object";
|
|
533
|
-
if (options?.validation !== false) value = this.schemaValidator.validate(schema, value, options?.validation);
|
|
534
|
-
if (as === "object") return value;
|
|
535
|
-
if (as === "binary") return codec.encodeToBinary(schema, value);
|
|
536
|
-
return codec.encodeToString(schema, value);
|
|
537
|
-
}
|
|
583
|
+
static DEFAULT_LONG_STRING_MAX_LENGTH = 1024;
|
|
538
584
|
/**
|
|
539
|
-
*
|
|
585
|
+
* Maximum length for rich strings, such as HTML or Markdown.
|
|
586
|
+
* This is a large value to accommodate rich text content.
|
|
587
|
+
* > It's also the maximum length of PG's TEXT type.
|
|
588
|
+
*
|
|
589
|
+
* It can be overridden in the string options.
|
|
590
|
+
*
|
|
591
|
+
* It can be set to a larger value:
|
|
592
|
+
* ```ts
|
|
593
|
+
* TypeProvider.DEFAULT_RICH_STRING_MAX_LENGTH = 1000000;
|
|
594
|
+
* ```
|
|
540
595
|
*/
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
596
|
+
static DEFAULT_RICH_STRING_MAX_LENGTH = 65535;
|
|
597
|
+
/**
|
|
598
|
+
* Maximum number of items in an array.
|
|
599
|
+
* This is a default value to prevent excessive memory usage.
|
|
600
|
+
* It can be overridden in the array options.
|
|
601
|
+
*/
|
|
602
|
+
static DEFAULT_ARRAY_MAX_ITEMS = 1e3;
|
|
603
|
+
raw = Type;
|
|
604
|
+
any = Type.Any;
|
|
605
|
+
void = Type.Void;
|
|
606
|
+
undefined = Type.Undefined;
|
|
607
|
+
record = Type.Record;
|
|
608
|
+
union = Type.Union;
|
|
609
|
+
tuple = Type.Tuple;
|
|
610
|
+
null = Type.Null;
|
|
611
|
+
const = Type.Literal;
|
|
612
|
+
options = Type.Options;
|
|
613
|
+
/**
|
|
614
|
+
* Type guards to check the type of schema.
|
|
615
|
+
* This is not a runtime type check, but a compile-time type guard.
|
|
616
|
+
*
|
|
617
|
+
* @example
|
|
618
|
+
* ```ts
|
|
619
|
+
* if (t.schema.isString(schema)) {
|
|
620
|
+
* // schema is TString
|
|
621
|
+
* }
|
|
622
|
+
* ```
|
|
623
|
+
*/
|
|
624
|
+
schema = new TypeGuard();
|
|
625
|
+
extend(schema, properties, options) {
|
|
626
|
+
return Type.Interface(Array.isArray(schema) ? schema : [schema], properties, {
|
|
627
|
+
additionalProperties: false,
|
|
628
|
+
...options
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
pick(schema, keys, options) {
|
|
632
|
+
return Type.Pick(schema, keys, {
|
|
633
|
+
additionalProperties: false,
|
|
634
|
+
...options
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
omit(schema, keys, options) {
|
|
638
|
+
return Type.Omit(schema, keys, {
|
|
639
|
+
additionalProperties: false,
|
|
640
|
+
...options
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
partial(schema, options) {
|
|
644
|
+
return Type.Partial(schema, {
|
|
645
|
+
additionalProperties: false,
|
|
646
|
+
...options
|
|
647
|
+
});
|
|
546
648
|
}
|
|
547
649
|
/**
|
|
548
|
-
*
|
|
650
|
+
* Create a schema for an object.
|
|
651
|
+
* By default, additional properties are not allowed.
|
|
549
652
|
*
|
|
550
|
-
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```ts
|
|
655
|
+
* const userSchema = t.object({
|
|
656
|
+
* id: t.integer(),
|
|
657
|
+
* name: t.string(),
|
|
658
|
+
* });
|
|
659
|
+
* ```
|
|
551
660
|
*/
|
|
552
|
-
|
|
553
|
-
return
|
|
661
|
+
object(properties, options) {
|
|
662
|
+
return Type.Object(properties, {
|
|
663
|
+
additionalProperties: false,
|
|
664
|
+
...options
|
|
665
|
+
});
|
|
554
666
|
}
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
//#endregion
|
|
558
|
-
//#region ../../src/core/providers/EventManager.ts
|
|
559
|
-
var EventManager = class {
|
|
560
|
-
logFn;
|
|
561
667
|
/**
|
|
562
|
-
*
|
|
668
|
+
* Create a schema for an array.
|
|
669
|
+
* By default, the maximum number of items is limited to prevent excessive memory usage.
|
|
670
|
+
*
|
|
671
|
+
* @see TypeProvider.DEFAULT_ARRAY_MAX_ITEMS
|
|
563
672
|
*/
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
673
|
+
array(schema, options) {
|
|
674
|
+
return Type.Array(schema, {
|
|
675
|
+
maxItems: TypeProvider.DEFAULT_ARRAY_MAX_ITEMS,
|
|
676
|
+
...options
|
|
677
|
+
});
|
|
567
678
|
}
|
|
568
|
-
|
|
569
|
-
|
|
679
|
+
/**
|
|
680
|
+
* Create a schema for a string.
|
|
681
|
+
* For db or input fields, consider using `t.text()` instead, which has length limits.
|
|
682
|
+
*
|
|
683
|
+
* If you need a string with specific format (e.g. email, uuid), consider using the corresponding method (e.g. `t.email()`, `t.uuid()`).
|
|
684
|
+
*/
|
|
685
|
+
string(options = {}) {
|
|
686
|
+
return Type.String({ ...options });
|
|
570
687
|
}
|
|
571
688
|
/**
|
|
572
|
-
*
|
|
689
|
+
* Create a schema for a string with length limits.
|
|
690
|
+
* For internal strings without length limits, consider using `t.string()` instead.
|
|
691
|
+
*
|
|
692
|
+
* Default size is "regular", which has a max length of 255 characters.
|
|
573
693
|
*/
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
this.events[event] = this.events[event].filter((it) => it.callback !== hook.callback);
|
|
586
|
-
};
|
|
694
|
+
text(options = {}) {
|
|
695
|
+
const { size, ...rest } = options;
|
|
696
|
+
const maxLength = size === "short" ? TypeProvider.DEFAULT_SHORT_STRING_MAX_LENGTH : size === "long" ? TypeProvider.DEFAULT_LONG_STRING_MAX_LENGTH : size === "rich" ? TypeProvider.DEFAULT_RICH_STRING_MAX_LENGTH : TypeProvider.DEFAULT_STRING_MAX_LENGTH;
|
|
697
|
+
return Type.String({
|
|
698
|
+
maxLength,
|
|
699
|
+
"~options": {
|
|
700
|
+
trim: options.trim ?? true,
|
|
701
|
+
lowercase: options.lowercase ?? false
|
|
702
|
+
},
|
|
703
|
+
...rest
|
|
704
|
+
});
|
|
587
705
|
}
|
|
588
706
|
/**
|
|
589
|
-
*
|
|
707
|
+
* Create a schema for a JSON object.
|
|
708
|
+
* This is a record with string keys and any values.
|
|
590
709
|
*/
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (options.log) {
|
|
594
|
-
ctx.now = Date.now();
|
|
595
|
-
this.log?.trace(`${func} ...`);
|
|
596
|
-
}
|
|
597
|
-
let events = this.events[func] ?? [];
|
|
598
|
-
if (options.reverse) events = events.toReversed();
|
|
599
|
-
for (const hook of events) {
|
|
600
|
-
const name = hook.caller?.name ?? "unknown";
|
|
601
|
-
if (options.log) {
|
|
602
|
-
ctx.now2 = Date.now();
|
|
603
|
-
this.log?.trace(`${func}(${name}) ...`);
|
|
604
|
-
}
|
|
605
|
-
try {
|
|
606
|
-
await hook.callback(payload);
|
|
607
|
-
} catch (error) {
|
|
608
|
-
if (options.catch) {
|
|
609
|
-
this.log?.error(`${func}(${name}) ERROR`, error);
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
if (options.log) throw new AlephaError(`Failed during '${func}()' hook for service: ${name}`, { cause: error });
|
|
613
|
-
throw error;
|
|
614
|
-
}
|
|
615
|
-
if (options.log) this.log?.debug(`${func}(${name}) OK [${Date.now() - ctx.now2}ms]`);
|
|
616
|
-
}
|
|
617
|
-
if (options.log) this.log?.debug(`${func} OK [${Date.now() - ctx.now}ms]`);
|
|
710
|
+
json(options) {
|
|
711
|
+
return t.record(t.text(), t.any(), options);
|
|
618
712
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
* Define an atom for state management.
|
|
625
|
-
*
|
|
626
|
-
* Atom lets you define a piece of state with a name, schema, and default value.
|
|
627
|
-
*
|
|
628
|
-
* By default, Alepha state is just a simple key-value store.
|
|
629
|
-
* Using atoms allows you to have type safety, validation, and default values for your state.
|
|
630
|
-
*
|
|
631
|
-
* You control how state is structured and validated.
|
|
632
|
-
*
|
|
633
|
-
* Features:
|
|
634
|
-
* - Set a schema for validation
|
|
635
|
-
* - Set a default value for initial state
|
|
636
|
-
* - Rules, like read-only, custom validation, etc.
|
|
637
|
-
* - Automatic getter access in services with {@link $use}
|
|
638
|
-
* - SSR support (server state automatically serialized and hydrated on client)
|
|
639
|
-
* - React integration (useAtom hook for automatic component re-renders)
|
|
640
|
-
* - Middleware
|
|
641
|
-
* - Persistence adapters (localStorage, Redis, database, file system, cookie, etc.)
|
|
642
|
-
* - State migrations (version upgrades when schema changes)
|
|
643
|
-
* - Documentation generation & devtools integration
|
|
644
|
-
*
|
|
645
|
-
* Common use cases:
|
|
646
|
-
* - user preferences
|
|
647
|
-
* - feature flags
|
|
648
|
-
* - configuration options
|
|
649
|
-
* - session data
|
|
650
|
-
*
|
|
651
|
-
* Atom must contain only serializable data.
|
|
652
|
-
* Avoid storing complex objects like class instances, functions, or DOM elements.
|
|
653
|
-
* If you need to store complex data, consider using identifiers or references instead.
|
|
654
|
-
*/
|
|
655
|
-
const $atom = (options) => {
|
|
656
|
-
return new Atom(options);
|
|
657
|
-
};
|
|
658
|
-
var Atom = class {
|
|
659
|
-
options;
|
|
660
|
-
get schema() {
|
|
661
|
-
return this.options.schema;
|
|
713
|
+
/**
|
|
714
|
+
* Create a schema for a boolean.
|
|
715
|
+
*/
|
|
716
|
+
boolean(options) {
|
|
717
|
+
return Type.Boolean({ ...options });
|
|
662
718
|
}
|
|
663
|
-
|
|
664
|
-
|
|
719
|
+
/**
|
|
720
|
+
* Create a schema for a number.
|
|
721
|
+
*/
|
|
722
|
+
number(options) {
|
|
723
|
+
return Type.Number({ ...options });
|
|
665
724
|
}
|
|
666
|
-
|
|
667
|
-
|
|
725
|
+
/**
|
|
726
|
+
* Create a schema for an integer.
|
|
727
|
+
*/
|
|
728
|
+
integer(options) {
|
|
729
|
+
return Type.Integer({ ...options });
|
|
668
730
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
als = $inject(AlsProvider);
|
|
676
|
-
events = $inject(EventManager);
|
|
677
|
-
codec = $inject(JsonSchemaCodec);
|
|
678
|
-
atoms = /* @__PURE__ */ new Map();
|
|
679
|
-
store = {};
|
|
680
|
-
constructor(store = {}) {
|
|
681
|
-
this.store = store;
|
|
731
|
+
int32(options) {
|
|
732
|
+
return Type.Integer({
|
|
733
|
+
minimum: -2147483647,
|
|
734
|
+
maximum: 2147483647,
|
|
735
|
+
...options
|
|
736
|
+
});
|
|
682
737
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
738
|
+
/**
|
|
739
|
+
* Mimic a signed 64-bit integer.
|
|
740
|
+
*
|
|
741
|
+
* This is NOT a true int64, as JavaScript cannot represent all int64 values.
|
|
742
|
+
* It is a number that is an integer and between -9007199254740991 and 9007199254740991.
|
|
743
|
+
* Use `t.bigint()` for true int64 values represented as strings.
|
|
744
|
+
*/
|
|
745
|
+
int64(options) {
|
|
746
|
+
return Type.Number({
|
|
747
|
+
format: "int64",
|
|
748
|
+
multipleOf: 1,
|
|
749
|
+
minimum: -9007199254740991,
|
|
750
|
+
maximum: 9007199254740991,
|
|
751
|
+
...options
|
|
695
752
|
});
|
|
696
|
-
return atoms;
|
|
697
753
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
return this;
|
|
754
|
+
/**
|
|
755
|
+
* Make a schema optional.
|
|
756
|
+
*/
|
|
757
|
+
optional(schema) {
|
|
758
|
+
return Type.Optional(schema);
|
|
705
759
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return
|
|
760
|
+
/**
|
|
761
|
+
* Make a schema nullable.
|
|
762
|
+
*/
|
|
763
|
+
nullable(schema, options) {
|
|
764
|
+
return Type.Union([Type.Null(), schema], options);
|
|
711
765
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
return this;
|
|
766
|
+
/**
|
|
767
|
+
* Create a schema that maps all properties of an object schema to nullable.
|
|
768
|
+
*/
|
|
769
|
+
nullify = (schema, options) => Type.Mapped(Type.Identifier("K"), Type.KeyOf(schema), Type.Ref("K"), Type.Union([Type.Index(schema, Type.Ref("K")), Type.Null()]), options);
|
|
770
|
+
/**
|
|
771
|
+
* Create a schema for a string enum.
|
|
772
|
+
*/
|
|
773
|
+
enum(values, options) {
|
|
774
|
+
return Type.Unsafe(t.text({
|
|
775
|
+
enum: values,
|
|
776
|
+
pattern: values.map((v) => `^${v}$`).join("|"),
|
|
777
|
+
...options
|
|
778
|
+
}));
|
|
726
779
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
780
|
+
/**
|
|
781
|
+
* Create a schema for a bigint represented as a string.
|
|
782
|
+
* This is a string that validates bigint format (e.g. "123456789").
|
|
783
|
+
*/
|
|
784
|
+
bigint(options) {
|
|
785
|
+
return t.string({
|
|
786
|
+
...options,
|
|
787
|
+
format: "bigint"
|
|
788
|
+
});
|
|
730
789
|
}
|
|
731
790
|
/**
|
|
732
|
-
*
|
|
791
|
+
* Create a schema for a URL represented as a string.
|
|
733
792
|
*/
|
|
734
|
-
|
|
735
|
-
return
|
|
793
|
+
url(options) {
|
|
794
|
+
return this.string({
|
|
795
|
+
...options,
|
|
796
|
+
format: "url"
|
|
797
|
+
});
|
|
736
798
|
}
|
|
737
799
|
/**
|
|
738
|
-
*
|
|
800
|
+
* Create a schema for binary data represented as a base64 string.
|
|
739
801
|
*/
|
|
740
|
-
|
|
741
|
-
return this.
|
|
802
|
+
binary(options) {
|
|
803
|
+
return this.string({
|
|
804
|
+
...options,
|
|
805
|
+
format: "binary"
|
|
806
|
+
});
|
|
742
807
|
}
|
|
743
808
|
/**
|
|
744
|
-
*
|
|
809
|
+
* Create a schema for uuid.
|
|
745
810
|
*/
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
811
|
+
uuid(options) {
|
|
812
|
+
return this.string({
|
|
813
|
+
...options,
|
|
814
|
+
format: "uuid"
|
|
815
|
+
});
|
|
750
816
|
}
|
|
751
817
|
/**
|
|
752
|
-
*
|
|
818
|
+
* Create a schema for a file-like object.
|
|
819
|
+
*
|
|
820
|
+
* File like mimics the File API in browsers, but is adapted to work in Node.js as well.
|
|
821
|
+
*
|
|
822
|
+
* Implementation of file-like objects is handled by "alepha/file" package.
|
|
753
823
|
*/
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
824
|
+
file(options) {
|
|
825
|
+
return Type.Unsafe(Type.Any({
|
|
826
|
+
[OPTIONS]: options,
|
|
827
|
+
format: "binary"
|
|
828
|
+
}));
|
|
757
829
|
}
|
|
758
830
|
/**
|
|
759
|
-
*
|
|
831
|
+
* @experimental
|
|
760
832
|
*/
|
|
761
|
-
|
|
762
|
-
return
|
|
833
|
+
stream() {
|
|
834
|
+
return Type.Unsafe(Type.Any({
|
|
835
|
+
format: "stream",
|
|
836
|
+
type: "string"
|
|
837
|
+
}));
|
|
838
|
+
}
|
|
839
|
+
email(options) {
|
|
840
|
+
return this.text({
|
|
841
|
+
...options,
|
|
842
|
+
format: "email",
|
|
843
|
+
trim: true,
|
|
844
|
+
lowercase: true
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
e164(options) {
|
|
848
|
+
return this.text({
|
|
849
|
+
...options,
|
|
850
|
+
description: "Phone number in E.164 format, e.g. +1234567890.",
|
|
851
|
+
pattern: "^\\+[1-9]\\d{1,14}$"
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
bcp47(options) {
|
|
855
|
+
return this.text({
|
|
856
|
+
...options,
|
|
857
|
+
description: "BCP 47 language tag, e.g. en, en-US, fr, fr-CA.",
|
|
858
|
+
pattern: "^[a-z]{2,3}(?:-[A-Z]{2})?$"
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Create a schema for short text, such as names or titles.
|
|
863
|
+
* Default max length is 64 characters.
|
|
864
|
+
*/
|
|
865
|
+
shortText(options) {
|
|
866
|
+
return this.text({
|
|
867
|
+
size: "short",
|
|
868
|
+
...options
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Create a schema for long text, such as descriptions or comments.
|
|
873
|
+
* Default max length is 1024 characters.
|
|
874
|
+
*/
|
|
875
|
+
longText(options) {
|
|
876
|
+
return this.text({
|
|
877
|
+
size: "long",
|
|
878
|
+
...options
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Create a schema for rich text, such as HTML or Markdown.
|
|
883
|
+
* Default max length is 65535 characters.
|
|
884
|
+
*/
|
|
885
|
+
richText(options) {
|
|
886
|
+
return this.text({
|
|
887
|
+
size: "rich",
|
|
888
|
+
...options
|
|
889
|
+
});
|
|
763
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
* Create a schema for a string enum e.g. LIKE_THIS.
|
|
893
|
+
*/
|
|
894
|
+
snakeCase = (options) => this.text({
|
|
895
|
+
pattern: "^[A-Z_-]+$",
|
|
896
|
+
...options
|
|
897
|
+
});
|
|
898
|
+
/**
|
|
899
|
+
* Create a schema for an object with a value and label.
|
|
900
|
+
*/
|
|
901
|
+
valueLabel = (options) => this.object({
|
|
902
|
+
value: this.snakeCase({ description: "Machine-readable value." }),
|
|
903
|
+
label: this.text({ description: "Human-readable label." }),
|
|
904
|
+
description: this.optional(this.text({
|
|
905
|
+
description: "Description of the value.",
|
|
906
|
+
size: "long"
|
|
907
|
+
}))
|
|
908
|
+
}, options);
|
|
909
|
+
datetime = (options) => t.text({
|
|
910
|
+
...options,
|
|
911
|
+
format: "date-time"
|
|
912
|
+
});
|
|
913
|
+
date = (options) => t.text({
|
|
914
|
+
...options,
|
|
915
|
+
format: "date"
|
|
916
|
+
});
|
|
917
|
+
time = (options) => t.text({
|
|
918
|
+
...options,
|
|
919
|
+
format: "time"
|
|
920
|
+
});
|
|
921
|
+
duration = (options) => t.text({
|
|
922
|
+
...options,
|
|
923
|
+
format: "duration"
|
|
924
|
+
});
|
|
764
925
|
};
|
|
926
|
+
const t = new TypeProvider();
|
|
765
927
|
|
|
766
928
|
//#endregion
|
|
767
|
-
//#region ../../src/core/
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
* It is responsible for managing the lifecycle of services,
|
|
772
|
-
* handling dependency injection,
|
|
773
|
-
* and providing a unified interface for the application.
|
|
774
|
-
*
|
|
775
|
-
* @example
|
|
776
|
-
* ```ts
|
|
777
|
-
* import { Alepha, run } from "alepha";
|
|
778
|
-
*
|
|
779
|
-
* class MyService {
|
|
780
|
-
* // business logic here
|
|
781
|
-
* }
|
|
782
|
-
*
|
|
783
|
-
* const alepha = Alepha.create({
|
|
784
|
-
* // state, env, and other properties
|
|
785
|
-
* })
|
|
786
|
-
*
|
|
787
|
-
* alepha.with(MyService);
|
|
788
|
-
*
|
|
789
|
-
* run(alepha); // trigger .start (and .stop) automatically
|
|
790
|
-
* ```
|
|
791
|
-
*
|
|
792
|
-
* ### Alepha Factory
|
|
793
|
-
*
|
|
794
|
-
* Alepha.create() is an enhanced version of new Alepha().
|
|
795
|
-
* - It merges `process.env` with the provided state.env when available.
|
|
796
|
-
* - It populates the test hooks for Vitest or Jest environments when available.
|
|
797
|
-
*
|
|
798
|
-
* new Alepha() is fine if you don't need these helpers.
|
|
799
|
-
*
|
|
800
|
-
* ### Platforms & Environments
|
|
801
|
-
*
|
|
802
|
-
* Alepha is designed to work in various environments:
|
|
803
|
-
* - **Browser**: Runs in the browser, using the global `window` object.
|
|
804
|
-
* - **Serverless**: Runs in serverless environments like Vercel or Vite.
|
|
805
|
-
* - **Test**: Runs in test environments like Jest or Vitest.
|
|
806
|
-
* - **Production**: Runs in production environments, typically with NODE_ENV set to "production".
|
|
807
|
-
* * You can check the current environment using the following methods:
|
|
808
|
-
*
|
|
809
|
-
* - `isBrowser()`: Returns true if the App is running in a browser environment.
|
|
810
|
-
* - `isServerless()`: Returns true if the App is running in a serverless environment.
|
|
811
|
-
* - `isTest()`: Returns true if the App is running in a test environment.
|
|
812
|
-
* - `isProduction()`: Returns true if the App is running in a production environment.
|
|
813
|
-
*
|
|
814
|
-
* ### State & Environment
|
|
815
|
-
*
|
|
816
|
-
* The state of the Alepha container is stored in the `store` property.
|
|
817
|
-
* Most important property is `store.env`, which contains the environment variables.
|
|
818
|
-
*
|
|
819
|
-
* ```ts
|
|
820
|
-
* const alepha = Alepha.create({ env: { MY_VAR: "value" } });
|
|
821
|
-
*
|
|
822
|
-
* // You can access the environment variables using alepha.env
|
|
823
|
-
* console.log(alepha.env.MY_VAR); // "value"
|
|
824
|
-
*
|
|
825
|
-
* // But you should use $env() primitive to get typed values from the environment.
|
|
826
|
-
* class App {
|
|
827
|
-
* env = $env(
|
|
828
|
-
* t.object({
|
|
829
|
-
* MY_VAR: t.text(),
|
|
830
|
-
* })
|
|
831
|
-
* );
|
|
832
|
-
* }
|
|
833
|
-
* ```
|
|
834
|
-
*
|
|
835
|
-
* ### Modules
|
|
836
|
-
*
|
|
837
|
-
* Modules are a way to group services together.
|
|
838
|
-
* You can register a module using the `$module` primitive.
|
|
839
|
-
*
|
|
840
|
-
* ```ts
|
|
841
|
-
* import { $module } from "alepha";
|
|
842
|
-
*
|
|
843
|
-
* class MyLib {}
|
|
844
|
-
*
|
|
845
|
-
* const myModule = $module({
|
|
846
|
-
* name: "my.project.module",
|
|
847
|
-
* services: [MyLib],
|
|
848
|
-
* });
|
|
849
|
-
* ```
|
|
850
|
-
*
|
|
851
|
-
* Do not use modules for small applications.
|
|
852
|
-
*
|
|
853
|
-
* ### Hooks
|
|
854
|
-
*
|
|
855
|
-
* Hooks are a way to run async functions from all registered providers/services.
|
|
856
|
-
* You can register a hook using the `$hook` primitive.
|
|
857
|
-
*
|
|
858
|
-
* ```ts
|
|
859
|
-
* import { $hook } from "alepha";
|
|
860
|
-
*
|
|
861
|
-
* class App {
|
|
862
|
-
* log = $logger();
|
|
863
|
-
* onCustomerHook = $hook({
|
|
864
|
-
* on: "my:custom:hook",
|
|
865
|
-
* handler: () => {
|
|
866
|
-
* this.log?.info("App is being configured");
|
|
867
|
-
* },
|
|
868
|
-
* });
|
|
869
|
-
* }
|
|
870
|
-
*
|
|
871
|
-
* Alepha.create()
|
|
872
|
-
* .with(App)
|
|
873
|
-
* .start()
|
|
874
|
-
* .then(alepha => alepha.events.emit("my:custom:hook"));
|
|
875
|
-
* ```
|
|
876
|
-
*
|
|
877
|
-
* Hooks are fully typed. You can create your own hooks by using module augmentation:
|
|
878
|
-
*
|
|
879
|
-
* ```ts
|
|
880
|
-
* declare module "alepha" {
|
|
881
|
-
* interface Hooks {
|
|
882
|
-
* "my:custom:hook": {
|
|
883
|
-
* arg1: string;
|
|
884
|
-
* }
|
|
885
|
-
* }
|
|
886
|
-
* }
|
|
887
|
-
* ```
|
|
888
|
-
*
|
|
889
|
-
* @module alepha
|
|
890
|
-
*/
|
|
891
|
-
var Alepha = class Alepha {
|
|
929
|
+
//#region ../../src/core/providers/SchemaValidator.ts
|
|
930
|
+
var SchemaValidator = class {
|
|
931
|
+
cache = /* @__PURE__ */ new Map();
|
|
932
|
+
useEval = true;
|
|
892
933
|
/**
|
|
893
|
-
*
|
|
934
|
+
* Validate the value against the provided schema.
|
|
894
935
|
*
|
|
895
|
-
*
|
|
896
|
-
|
|
936
|
+
* Validation create a new value by applying some preprocessing. (e.g., trimming text)
|
|
937
|
+
*/
|
|
938
|
+
validate(schema, value, options = {}) {
|
|
939
|
+
const newValue = this.beforeParse(schema, value, {
|
|
940
|
+
trim: options.trim ?? true,
|
|
941
|
+
nullToUndefined: options.nullToUndefined ?? true,
|
|
942
|
+
deleteUndefined: options.deleteUndefined ?? true
|
|
943
|
+
});
|
|
944
|
+
try {
|
|
945
|
+
if (!this.useEval) return Value.Parse(schema, newValue);
|
|
946
|
+
return this.getValidator(schema).Parse(newValue);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
if (error.cause?.errors?.[0]) throw new TypeBoxError(error.cause.errors[0]);
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
getValidator(schema) {
|
|
953
|
+
if (this.cache.has(schema)) return this.cache.get(schema);
|
|
954
|
+
const validator = Compile(schema);
|
|
955
|
+
this.cache.set(schema, validator);
|
|
956
|
+
return validator;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Preprocess the value based on the schema before validation.
|
|
897
960
|
*
|
|
898
|
-
* If
|
|
961
|
+
* - If the value is `null` and the schema does not allow `null`, it converts it to `undefined`.
|
|
962
|
+
* - If the value is a string and the schema has a `~options.trim` flag, it trims whitespace from the string.
|
|
899
963
|
*/
|
|
900
|
-
|
|
901
|
-
if (
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (
|
|
964
|
+
beforeParse(schema, value, options) {
|
|
965
|
+
if (!schema) return value;
|
|
966
|
+
if (value === null && !this.isSchemaNullable(schema) && options.nullToUndefined) return;
|
|
967
|
+
if (Array.isArray(value)) return value.map((it) => this.beforeParse(schema.items, it, options));
|
|
968
|
+
if (typeof value === "string" && schema.type === "string") {
|
|
969
|
+
let str = value;
|
|
970
|
+
if (options.trim && schema["~options"]?.trim) str = str.trim();
|
|
971
|
+
if (schema["~options"]?.lowercase) str = str.toLowerCase();
|
|
972
|
+
return str;
|
|
907
973
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
afterAll?.(() => alepha.stop());
|
|
917
|
-
try {
|
|
918
|
-
onTestFinished?.(() => alepha.stop());
|
|
919
|
-
} catch (_error) {}
|
|
920
|
-
alepha.store.set("alepha.test.beforeAll", beforeAll).set("alepha.test.afterAll", afterAll).set("alepha.test.afterEach", afterEach).set("alepha.test.onTestFinished", onTestFinished);
|
|
974
|
+
if (typeof value === "object" && value !== null && schema.type === "object") {
|
|
975
|
+
const obj = {};
|
|
976
|
+
for (const key in value) {
|
|
977
|
+
const newValue = this.beforeParse(schema.properties?.[key], value[key], options);
|
|
978
|
+
if (newValue === void 0 && options.deleteUndefined) continue;
|
|
979
|
+
obj[key] = newValue;
|
|
980
|
+
}
|
|
981
|
+
return obj;
|
|
921
982
|
}
|
|
922
|
-
return
|
|
983
|
+
return value;
|
|
923
984
|
}
|
|
924
985
|
/**
|
|
925
|
-
*
|
|
926
|
-
* Pass to true when #start() is called.
|
|
986
|
+
* Used by `beforeParse` to determine if a schema allows null values.
|
|
927
987
|
*/
|
|
928
|
-
|
|
988
|
+
isSchemaNullable = (schema) => {
|
|
989
|
+
if (!schema) return false;
|
|
990
|
+
if (schema.type === "null") return true;
|
|
991
|
+
if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
|
|
992
|
+
if (schema.anyOf) return schema.anyOf.some((it) => this.isSchemaNullable(it));
|
|
993
|
+
if (schema.oneOf) return schema.oneOf.some((it) => this.isSchemaNullable(it));
|
|
994
|
+
if (schema.allOf) return schema.allOf.some((it) => this.isSchemaNullable(it));
|
|
995
|
+
return false;
|
|
996
|
+
};
|
|
997
|
+
onConfigure = $hook({
|
|
998
|
+
on: "configure",
|
|
999
|
+
handler: () => {
|
|
1000
|
+
this.useEval = this.canEval();
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
canEval() {
|
|
1004
|
+
try {
|
|
1005
|
+
Compile(t.object({ test: t.string() })).Parse({ test: "value" });
|
|
1006
|
+
return true;
|
|
1007
|
+
} catch {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
//#endregion
|
|
1014
|
+
//#region ../../src/core/providers/CodecManager.ts
|
|
1015
|
+
/**
|
|
1016
|
+
* CodecManager manages multiple codec formats and provides a unified interface
|
|
1017
|
+
* for encoding and decoding data with different formats.
|
|
1018
|
+
*/
|
|
1019
|
+
var CodecManager = class {
|
|
1020
|
+
codecs = /* @__PURE__ */ new Map();
|
|
1021
|
+
jsonCodec = $inject(JsonSchemaCodec);
|
|
1022
|
+
schemaValidator = $inject(SchemaValidator);
|
|
1023
|
+
default = "json";
|
|
1024
|
+
constructor() {
|
|
1025
|
+
this.register(this.default, this.jsonCodec);
|
|
1026
|
+
}
|
|
929
1027
|
/**
|
|
930
|
-
*
|
|
1028
|
+
* Register a new codec format.
|
|
1029
|
+
*
|
|
1030
|
+
* @param name - The name of the codec (e.g., 'json', 'protobuf')
|
|
1031
|
+
* @param codec - The codec implementation
|
|
931
1032
|
*/
|
|
932
|
-
|
|
1033
|
+
register(name, codec) {
|
|
1034
|
+
this.codecs.set(name, codec);
|
|
1035
|
+
}
|
|
933
1036
|
/**
|
|
934
|
-
*
|
|
1037
|
+
* Get a specific codec by name.
|
|
1038
|
+
*
|
|
1039
|
+
* @param name - The name of the codec
|
|
1040
|
+
* @returns The codec instance
|
|
1041
|
+
* @throws {AlephaError} If the codec is not found
|
|
935
1042
|
*/
|
|
936
|
-
|
|
1043
|
+
getCodec(name) {
|
|
1044
|
+
const codec = this.codecs.get(name);
|
|
1045
|
+
if (!codec) throw new AlephaError(`Codec "${name}" not found. Available codecs: ${Array.from(this.codecs.keys()).join(", ")}`);
|
|
1046
|
+
return codec;
|
|
1047
|
+
}
|
|
937
1048
|
/**
|
|
938
|
-
*
|
|
1049
|
+
* Encode data using the specified codec and output format.
|
|
939
1050
|
*/
|
|
940
|
-
|
|
1051
|
+
encode(schema, value, options) {
|
|
1052
|
+
const codec = this.getCodec(options?.encoder ?? this.default);
|
|
1053
|
+
const as = options?.as ?? "object";
|
|
1054
|
+
if (options?.validation !== false) value = this.schemaValidator.validate(schema, value, options?.validation);
|
|
1055
|
+
if (as === "object") return value;
|
|
1056
|
+
if (as === "binary") return codec.encodeToBinary(schema, value);
|
|
1057
|
+
return codec.encodeToString(schema, value);
|
|
1058
|
+
}
|
|
941
1059
|
/**
|
|
942
|
-
*
|
|
1060
|
+
* Decode data using the specified codec.
|
|
943
1061
|
*/
|
|
944
|
-
|
|
1062
|
+
decode(schema, data, options) {
|
|
1063
|
+
const encoderName = options?.encoder ?? this.default;
|
|
1064
|
+
let value = this.getCodec(encoderName).decode(schema, data);
|
|
1065
|
+
if (options?.validation !== false) value = this.schemaValidator.validate(schema, value, options?.validation);
|
|
1066
|
+
return value;
|
|
1067
|
+
}
|
|
945
1068
|
/**
|
|
946
|
-
*
|
|
947
|
-
*
|
|
1069
|
+
* Validate decoded data against the schema.
|
|
1070
|
+
*
|
|
1071
|
+
* This is automatically called before encoding or after decoding.
|
|
948
1072
|
*/
|
|
949
|
-
|
|
1073
|
+
validate(schema, value, options) {
|
|
1074
|
+
return this.schemaValidator.validate(schema, value, options);
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
//#endregion
|
|
1079
|
+
//#region ../../src/core/providers/EventManager.ts
|
|
1080
|
+
var EventManager = class {
|
|
1081
|
+
logFn;
|
|
950
1082
|
/**
|
|
951
|
-
*
|
|
952
|
-
* > It allows us to avoid parsing the same schema multiple times.
|
|
1083
|
+
* List of events that can be triggered. Powered by $hook().
|
|
953
1084
|
*/
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
/**
|
|
962
|
-
* List of service substitutions.
|
|
963
|
-
*
|
|
964
|
-
* Services registered here will be replaced by the specified service when injected.
|
|
965
|
-
*/
|
|
966
|
-
substitutions = /* @__PURE__ */ new Map();
|
|
967
|
-
/**
|
|
968
|
-
* Registry of primitives.
|
|
969
|
-
*/
|
|
970
|
-
primitiveRegistry = /* @__PURE__ */ new Map();
|
|
971
|
-
/**
|
|
972
|
-
* List of all services + how they are provided.
|
|
973
|
-
*/
|
|
974
|
-
registry = /* @__PURE__ */ new Map();
|
|
975
|
-
/**
|
|
976
|
-
* Node.js feature that allows to store context across asynchronous calls.
|
|
977
|
-
*
|
|
978
|
-
* This is used for logging, tracing, and other context-related features.
|
|
979
|
-
*
|
|
980
|
-
* Mocked for browser environments.
|
|
981
|
-
*/
|
|
982
|
-
context;
|
|
983
|
-
/**
|
|
984
|
-
* Event manager to handle lifecycle events and custom events.
|
|
985
|
-
*/
|
|
986
|
-
events;
|
|
987
|
-
/**
|
|
988
|
-
* State manager to store arbitrary values.
|
|
989
|
-
*/
|
|
990
|
-
store;
|
|
991
|
-
/**
|
|
992
|
-
* Codec manager for encoding and decoding data with different formats.
|
|
993
|
-
*
|
|
994
|
-
* Supports multiple codec formats (JSON, Protobuf, etc.) with a unified interface.
|
|
995
|
-
*/
|
|
996
|
-
codec;
|
|
1085
|
+
events = {};
|
|
1086
|
+
constructor(logFn) {
|
|
1087
|
+
this.logFn = logFn;
|
|
1088
|
+
}
|
|
1089
|
+
get log() {
|
|
1090
|
+
return this.logFn?.();
|
|
1091
|
+
}
|
|
997
1092
|
/**
|
|
998
|
-
*
|
|
1093
|
+
* Registers a hook for the specified event.
|
|
999
1094
|
*/
|
|
1000
|
-
|
|
1001
|
-
|
|
1095
|
+
on(event, hookOrFunc) {
|
|
1096
|
+
if (!this.events[event]) this.events[event] = [];
|
|
1097
|
+
const hook = typeof hookOrFunc === "function" ? { callback: hookOrFunc } : hookOrFunc;
|
|
1098
|
+
if (hook.priority === "first") this.events[event].unshift(hook);
|
|
1099
|
+
else if (hook.priority === "last") this.events[event].push(hook);
|
|
1100
|
+
else {
|
|
1101
|
+
const index = this.events[event].findIndex((it) => it.priority === "last");
|
|
1102
|
+
if (index !== -1) this.events[event].splice(index, 0, hook);
|
|
1103
|
+
else this.events[event].push(hook);
|
|
1104
|
+
}
|
|
1105
|
+
return () => {
|
|
1106
|
+
this.events[event] = this.events[event].filter((it) => it.callback !== hook.callback);
|
|
1107
|
+
};
|
|
1002
1108
|
}
|
|
1003
1109
|
/**
|
|
1004
|
-
*
|
|
1110
|
+
* Emits the specified event with the given payload.
|
|
1005
1111
|
*/
|
|
1006
|
-
|
|
1007
|
-
|
|
1112
|
+
async emit(func, payload, options = {}) {
|
|
1113
|
+
const ctx = {};
|
|
1114
|
+
if (options.log) {
|
|
1115
|
+
ctx.now = Date.now();
|
|
1116
|
+
this.log?.trace(`${func} ...`);
|
|
1117
|
+
}
|
|
1118
|
+
let events = this.events[func] ?? [];
|
|
1119
|
+
if (options.reverse) events = events.toReversed();
|
|
1120
|
+
for (const hook of events) {
|
|
1121
|
+
const name = hook.caller?.name ?? "unknown";
|
|
1122
|
+
if (options.log) {
|
|
1123
|
+
ctx.now2 = Date.now();
|
|
1124
|
+
this.log?.trace(`${func}(${name}) ...`);
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
await hook.callback(payload);
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (options.catch) {
|
|
1130
|
+
this.log?.error(`${func}(${name}) ERROR`, error);
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (options.log) throw new AlephaError(`Failed during '${func}()' hook for service: ${name}`, { cause: error });
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
if (options.log) this.log?.debug(`${func}(${name}) OK [${Date.now() - ctx.now2}ms]`);
|
|
1137
|
+
}
|
|
1138
|
+
if (options.log) this.log?.debug(`${func} OK [${Date.now() - ctx.now}ms]`);
|
|
1008
1139
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region ../../src/core/primitives/$atom.ts
|
|
1144
|
+
/**
|
|
1145
|
+
* Define an atom for state management.
|
|
1146
|
+
*
|
|
1147
|
+
* Atom lets you define a piece of state with a name, schema, and default value.
|
|
1148
|
+
*
|
|
1149
|
+
* By default, Alepha state is just a simple key-value store.
|
|
1150
|
+
* Using atoms allows you to have type safety, validation, and default values for your state.
|
|
1151
|
+
*
|
|
1152
|
+
* You control how state is structured and validated.
|
|
1153
|
+
*
|
|
1154
|
+
* Features:
|
|
1155
|
+
* - Set a schema for validation
|
|
1156
|
+
* - Set a default value for initial state
|
|
1157
|
+
* - Rules, like read-only, custom validation, etc.
|
|
1158
|
+
* - Automatic getter access in services with {@link $use}
|
|
1159
|
+
* - SSR support (server state automatically serialized and hydrated on client)
|
|
1160
|
+
* - React integration (useAtom hook for automatic component re-renders)
|
|
1161
|
+
* - Middleware
|
|
1162
|
+
* - Persistence adapters (localStorage, Redis, database, file system, cookie, etc.)
|
|
1163
|
+
* - State migrations (version upgrades when schema changes)
|
|
1164
|
+
* - Documentation generation & devtools integration
|
|
1165
|
+
*
|
|
1166
|
+
* Common use cases:
|
|
1167
|
+
* - user preferences
|
|
1168
|
+
* - feature flags
|
|
1169
|
+
* - configuration options
|
|
1170
|
+
* - session data
|
|
1171
|
+
*
|
|
1172
|
+
* Atom must contain only serializable data.
|
|
1173
|
+
* Avoid storing complex objects like class instances, functions, or DOM elements.
|
|
1174
|
+
* If you need to store complex data, consider using identifiers or references instead.
|
|
1175
|
+
*/
|
|
1176
|
+
const $atom = (options) => {
|
|
1177
|
+
return new Atom(options);
|
|
1178
|
+
};
|
|
1179
|
+
var Atom = class {
|
|
1180
|
+
options;
|
|
1181
|
+
get schema() {
|
|
1182
|
+
return this.options.schema;
|
|
1015
1183
|
}
|
|
1016
|
-
|
|
1017
|
-
this.
|
|
1184
|
+
get key() {
|
|
1185
|
+
return this.options.name;
|
|
1186
|
+
}
|
|
1187
|
+
constructor(options) {
|
|
1188
|
+
this.options = options;
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
$atom[KIND] = "atom";
|
|
1192
|
+
|
|
1193
|
+
//#endregion
|
|
1194
|
+
//#region ../../src/core/providers/StateManager.ts
|
|
1195
|
+
var StateManager = class {
|
|
1196
|
+
als = $inject(AlsProvider);
|
|
1197
|
+
events = $inject(EventManager);
|
|
1198
|
+
codec = $inject(JsonSchemaCodec);
|
|
1199
|
+
atoms = /* @__PURE__ */ new Map();
|
|
1200
|
+
store = {};
|
|
1201
|
+
constructor(store = {}) {
|
|
1202
|
+
this.store = store;
|
|
1203
|
+
}
|
|
1204
|
+
getAtoms(context = true) {
|
|
1205
|
+
const atoms = [];
|
|
1206
|
+
if (context && this.als?.exists()) for (const atom of this.atoms.values()) {
|
|
1207
|
+
const value = this.als.get(atom.key);
|
|
1208
|
+
if (value !== void 0) atoms.push({
|
|
1209
|
+
atom,
|
|
1210
|
+
value
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
else for (const [key, atom] of this.atoms.entries()) atoms.push({
|
|
1214
|
+
atom,
|
|
1215
|
+
value: this.store[key]
|
|
1216
|
+
});
|
|
1217
|
+
return atoms;
|
|
1218
|
+
}
|
|
1219
|
+
register(atom) {
|
|
1220
|
+
const key = atom.key;
|
|
1221
|
+
if (!this.atoms.has(key)) {
|
|
1222
|
+
this.atoms.set(key, atom);
|
|
1223
|
+
if (!(key in this.store)) this.set(key, atom.options.default, { skipContext: true });
|
|
1224
|
+
}
|
|
1018
1225
|
return this;
|
|
1019
1226
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
isLocked() {
|
|
1026
|
-
return this.locked;
|
|
1227
|
+
get(target) {
|
|
1228
|
+
if (target instanceof Atom) this.register(target);
|
|
1229
|
+
const key = target instanceof Atom ? target.key : target;
|
|
1230
|
+
const store = this.store;
|
|
1231
|
+
return this.als?.exists() ? this.als.get(key) ?? store[key] : store[key];
|
|
1027
1232
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1233
|
+
set(target, value, options) {
|
|
1234
|
+
if (target instanceof Atom) this.register(target);
|
|
1235
|
+
const key = target instanceof Atom ? target.key : target;
|
|
1236
|
+
const store = this.store;
|
|
1237
|
+
const prevValue = this.get(key);
|
|
1238
|
+
if (prevValue === value) return this;
|
|
1239
|
+
if (options?.skipContext !== true && this.als?.exists()) this.als.set(key, value);
|
|
1240
|
+
else store[key] = value;
|
|
1241
|
+
if (options?.skipEvents !== true) this.events?.emit("state:mutate", {
|
|
1242
|
+
key,
|
|
1243
|
+
value,
|
|
1244
|
+
prevValue
|
|
1245
|
+
}, { catch: true }).catch(() => null);
|
|
1246
|
+
return this;
|
|
1247
|
+
}
|
|
1248
|
+
mut(target, mutator) {
|
|
1249
|
+
const updated = mutator(this.get(target));
|
|
1250
|
+
return this.set(target, updated);
|
|
1037
1251
|
}
|
|
1038
1252
|
/**
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
* It means that #start() has been called but maybe not all services are ready.
|
|
1253
|
+
* Check if a key exists in the state
|
|
1042
1254
|
*/
|
|
1043
|
-
|
|
1044
|
-
return this.
|
|
1255
|
+
has(key) {
|
|
1256
|
+
return key in this.store;
|
|
1045
1257
|
}
|
|
1046
1258
|
/**
|
|
1047
|
-
*
|
|
1259
|
+
* Delete a key from the state (set to undefined)
|
|
1048
1260
|
*/
|
|
1049
|
-
|
|
1050
|
-
return this.
|
|
1261
|
+
del(key) {
|
|
1262
|
+
return this.set(key, void 0);
|
|
1051
1263
|
}
|
|
1052
1264
|
/**
|
|
1053
|
-
*
|
|
1265
|
+
* Push a value to an array in the state
|
|
1054
1266
|
*/
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1267
|
+
push(key, ...value) {
|
|
1268
|
+
const current = this.get(key) ?? [];
|
|
1269
|
+
if (Array.isArray(current)) this.set(key, [...current, ...value]);
|
|
1270
|
+
return this;
|
|
1058
1271
|
}
|
|
1059
1272
|
/**
|
|
1060
|
-
*
|
|
1273
|
+
* Clear all state
|
|
1061
1274
|
*/
|
|
1062
|
-
|
|
1063
|
-
|
|
1275
|
+
clear() {
|
|
1276
|
+
this.store = {};
|
|
1277
|
+
return this;
|
|
1064
1278
|
}
|
|
1065
1279
|
/**
|
|
1066
|
-
*
|
|
1067
|
-
*/
|
|
1068
|
-
isViteDev() {
|
|
1069
|
-
if (this.isBrowser()) return false;
|
|
1070
|
-
return !!this.env.VITE_ALEPHA_DEV;
|
|
1071
|
-
}
|
|
1072
|
-
isBun() {
|
|
1073
|
-
return "Bun" in globalThis;
|
|
1074
|
-
}
|
|
1075
|
-
/**
|
|
1076
|
-
* Returns whether the App is running in a serverless environment.
|
|
1077
|
-
*/
|
|
1078
|
-
isServerless() {
|
|
1079
|
-
if (this.isBrowser()) return false;
|
|
1080
|
-
if (this.env.VERCEL_REGION) return true;
|
|
1081
|
-
if (typeof global === "object" && typeof global.Cloudflare === "object") return true;
|
|
1082
|
-
return false;
|
|
1083
|
-
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Returns whether the App is in test mode. (Running in a test environment)
|
|
1086
|
-
*
|
|
1087
|
-
* > This is automatically set when running tests with Jest or Vitest.
|
|
1088
|
-
*/
|
|
1089
|
-
isTest() {
|
|
1090
|
-
return this.env.NODE_ENV === "test";
|
|
1091
|
-
}
|
|
1092
|
-
/**
|
|
1093
|
-
* Returns whether the App is in production mode. (Running in a production environment)
|
|
1094
|
-
*
|
|
1095
|
-
* > This is automatically set by Vite or Vercel. However, you have to set it manually when running Docker apps.
|
|
1096
|
-
*/
|
|
1097
|
-
isProduction() {
|
|
1098
|
-
return this.env.NODE_ENV === "production";
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Starts the App.
|
|
1102
|
-
*
|
|
1103
|
-
* - Lock any further changes to the container.
|
|
1104
|
-
* - Run "configure" hook for all services. Primitives will be processed.
|
|
1105
|
-
* - Run "start" hook for all services. Providers will connect/listen/...
|
|
1106
|
-
* - Run "ready" hook for all services. This is the point where the App is ready to serve requests.
|
|
1107
|
-
*
|
|
1108
|
-
* @return A promise that resolves when the App has started.
|
|
1109
|
-
*/
|
|
1110
|
-
async start() {
|
|
1111
|
-
if (this.ready) {
|
|
1112
|
-
this.log?.debug("App is already started, skipping...");
|
|
1113
|
-
return this;
|
|
1114
|
-
}
|
|
1115
|
-
if (this.starting) {
|
|
1116
|
-
this.log?.warn("App is already starting, waiting for it to finish...");
|
|
1117
|
-
return this.starting.promise;
|
|
1118
|
-
}
|
|
1119
|
-
this.starting = Promise.withResolvers();
|
|
1120
|
-
const now = Date.now();
|
|
1121
|
-
this.log?.info("Starting App...");
|
|
1122
|
-
for (const [key] of this.substitutions.entries()) this.inject(key);
|
|
1123
|
-
const target = this.store.get("alepha.target");
|
|
1124
|
-
if (target) {
|
|
1125
|
-
this.registry = /* @__PURE__ */ new Map();
|
|
1126
|
-
this.primitiveRegistry = /* @__PURE__ */ new Map();
|
|
1127
|
-
this.with(target);
|
|
1128
|
-
}
|
|
1129
|
-
this.locked = true;
|
|
1130
|
-
await this.events.emit("configure", this, { log: true });
|
|
1131
|
-
this.configured = true;
|
|
1132
|
-
await this.events.emit("start", this, { log: true });
|
|
1133
|
-
this.started = true;
|
|
1134
|
-
await this.events.emit("ready", this, { log: true });
|
|
1135
|
-
this.log?.info(`App is now ready [${Date.now() - now}ms]`);
|
|
1136
|
-
this.ready = true;
|
|
1137
|
-
this.starting.resolve(this);
|
|
1138
|
-
this.starting = void 0;
|
|
1139
|
-
return this;
|
|
1140
|
-
}
|
|
1141
|
-
/**
|
|
1142
|
-
* Stops the App.
|
|
1143
|
-
*
|
|
1144
|
-
* - Run "stop" hook for all services.
|
|
1145
|
-
*
|
|
1146
|
-
* Stop will NOT reset the container.
|
|
1147
|
-
* Stop will NOT unlock the container.
|
|
1148
|
-
*
|
|
1149
|
-
* > Stop is used to gracefully shut down the application, nothing more. There is no "restart".
|
|
1150
|
-
*
|
|
1151
|
-
* @return A promise that resolves when the App has stopped.
|
|
1152
|
-
*/
|
|
1153
|
-
async stop() {
|
|
1154
|
-
if (!this.started) return;
|
|
1155
|
-
this.log?.info("Stopping App...");
|
|
1156
|
-
await this.events.emit("stop", this, {
|
|
1157
|
-
reverse: true,
|
|
1158
|
-
log: true
|
|
1159
|
-
});
|
|
1160
|
-
this.log?.info("App is now off");
|
|
1161
|
-
this.started = false;
|
|
1162
|
-
this.ready = false;
|
|
1163
|
-
}
|
|
1164
|
-
/**
|
|
1165
|
-
* Check if entry is registered in the container.
|
|
1166
|
-
*/
|
|
1167
|
-
has(entry, opts = {}) {
|
|
1168
|
-
if (entry === Alepha) return true;
|
|
1169
|
-
const { inStack = true, inRegistry = true, inSubstitutions = true, registry = this.registry } = opts;
|
|
1170
|
-
const { provide } = typeof entry === "object" && "provide" in entry ? entry : { provide: entry };
|
|
1171
|
-
if (inSubstitutions) {
|
|
1172
|
-
if (this.substitutions.get(provide)) return true;
|
|
1173
|
-
}
|
|
1174
|
-
if (inRegistry) {
|
|
1175
|
-
if (registry.get(provide)) return true;
|
|
1176
|
-
}
|
|
1177
|
-
if (inStack) {
|
|
1178
|
-
const substitute = this.substitutions.get(provide)?.use;
|
|
1179
|
-
if (substitute && this.pendingInstantiations.includes(substitute)) return true;
|
|
1180
|
-
return this.pendingInstantiations.includes(provide);
|
|
1181
|
-
}
|
|
1182
|
-
return false;
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* Registers the specified service in the container.
|
|
1186
|
-
*
|
|
1187
|
-
* - If the service is ALREADY registered, the method does nothing.
|
|
1188
|
-
* - If the service is NOT registered, a new instance is created and registered.
|
|
1189
|
-
*
|
|
1190
|
-
* Method is chainable, so you can register multiple services in a single call.
|
|
1191
|
-
*
|
|
1192
|
-
* > ServiceEntry allows to provide a service **substitution** feature.
|
|
1193
|
-
*
|
|
1194
|
-
* @example
|
|
1195
|
-
* ```ts
|
|
1196
|
-
* class A { value = "a"; }
|
|
1197
|
-
* class B { value = "b"; }
|
|
1198
|
-
* class M { a = $inject(A); }
|
|
1199
|
-
*
|
|
1200
|
-
* Alepha.create().with({ provide: A, use: B }).get(M).a.value; // "b"
|
|
1201
|
-
* ```
|
|
1202
|
-
*
|
|
1203
|
-
* > **Substitution** is an advanced feature that allows you to replace a service with another service.
|
|
1204
|
-
* > It's useful for testing or for providing different implementations of a service.
|
|
1205
|
-
* > If you are interested in configuring a service, use Alepha#configure() instead.
|
|
1206
|
-
*
|
|
1207
|
-
* @param serviceEntry - The service to register in the container.
|
|
1208
|
-
* @return Current instance of Alepha.
|
|
1209
|
-
*/
|
|
1210
|
-
with(serviceEntry) {
|
|
1211
|
-
const entry = "default" in serviceEntry ? serviceEntry.default : serviceEntry;
|
|
1212
|
-
if (this.has(entry, {
|
|
1213
|
-
inSubstitutions: false,
|
|
1214
|
-
inRegistry: false
|
|
1215
|
-
})) return this;
|
|
1216
|
-
if (typeof entry === "object") {
|
|
1217
|
-
if (entry.provide === entry.use) {
|
|
1218
|
-
this.inject(entry.provide);
|
|
1219
|
-
return this;
|
|
1220
|
-
}
|
|
1221
|
-
if (!this.substitutions.has(entry.provide) && !this.has(entry.provide)) {
|
|
1222
|
-
if (this.started) throw new ContainerLockedError();
|
|
1223
|
-
if (MODULE in entry.provide && typeof entry.provide[MODULE] === "function") entry.use[MODULE] ??= entry.provide[MODULE];
|
|
1224
|
-
this.substitutions.set(entry.provide, { use: entry.use });
|
|
1225
|
-
} else if (!entry.optional) throw new TooLateSubstitutionError(entry.provide.name, entry.use.name);
|
|
1226
|
-
return this;
|
|
1227
|
-
}
|
|
1228
|
-
this.inject(entry);
|
|
1229
|
-
return this;
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Get an instance of the specified service from the container.
|
|
1233
|
-
*
|
|
1234
|
-
* @see {@link InjectOptions} for the available options.
|
|
1235
|
-
*/
|
|
1236
|
-
inject(service, opts = {}) {
|
|
1237
|
-
const lifetime = opts.lifetime ?? "singleton";
|
|
1238
|
-
const parent = opts.parent !== void 0 ? opts.parent : __alephaRef?.parent ?? Alepha;
|
|
1239
|
-
const transient = lifetime === "transient";
|
|
1240
|
-
const registry = lifetime === "scoped" ? this.context.get("registry") ?? this.registry : this.registry;
|
|
1241
|
-
if (service === Alepha) return this;
|
|
1242
|
-
if (typeof service === "string") {
|
|
1243
|
-
for (const [key, value] of registry.entries()) if (key.name === service) return value.instance;
|
|
1244
|
-
throw new AlephaError(`Service not found: ${service}`);
|
|
1245
|
-
}
|
|
1246
|
-
const substitute = this.substitutions.get(service);
|
|
1247
|
-
if (substitute) return this.inject(substitute.use, {
|
|
1248
|
-
parent,
|
|
1249
|
-
lifetime
|
|
1250
|
-
});
|
|
1251
|
-
const index = this.pendingInstantiations.indexOf(service);
|
|
1252
|
-
if (index !== -1 && !transient) throw new CircularDependencyError(service.name, this.pendingInstantiations.slice(0, index).map((it) => it.name));
|
|
1253
|
-
if (!transient) {
|
|
1254
|
-
const match = registry.get(service);
|
|
1255
|
-
if (match) {
|
|
1256
|
-
if (!match.parents.includes(parent) && parent !== service) match.parents.push(parent);
|
|
1257
|
-
return match.instance;
|
|
1258
|
-
}
|
|
1259
|
-
if (this.started) throw new ContainerLockedError(`Container is locked. No more services can be added. ${parent?.name} -> ${service.name}`);
|
|
1260
|
-
}
|
|
1261
|
-
const module = service[MODULE];
|
|
1262
|
-
if (module && typeof module === "function") this.with(module);
|
|
1263
|
-
if (this.has(service, { registry }) && !transient) return this.inject(service, {
|
|
1264
|
-
parent,
|
|
1265
|
-
lifetime
|
|
1266
|
-
});
|
|
1267
|
-
const instance = this.new(service, opts.args);
|
|
1268
|
-
const definition = {
|
|
1269
|
-
parents: [parent],
|
|
1270
|
-
instance
|
|
1271
|
-
};
|
|
1272
|
-
if (!transient) registry.set(service, definition);
|
|
1273
|
-
if (instance instanceof Module) {
|
|
1274
|
-
this.modules.push(instance);
|
|
1275
|
-
const parent$1 = __alephaRef.parent;
|
|
1276
|
-
__alephaRef.parent = instance.constructor;
|
|
1277
|
-
instance.register(this);
|
|
1278
|
-
__alephaRef.parent = parent$1;
|
|
1279
|
-
}
|
|
1280
|
-
return instance;
|
|
1281
|
-
}
|
|
1282
|
-
/**
|
|
1283
|
-
* Applies environment variables to the provided schema and state object.
|
|
1284
|
-
*
|
|
1285
|
-
* It replaces also all templated $ENV inside string values.
|
|
1286
|
-
*
|
|
1287
|
-
* @param schema - The schema object to apply environment variables to.
|
|
1288
|
-
* @return The schema object with environment variables applied.
|
|
1289
|
-
*/
|
|
1290
|
-
parseEnv(schema) {
|
|
1291
|
-
if (this.cacheEnv.has(schema)) return this.cacheEnv.get(schema);
|
|
1292
|
-
const config = this.codec.validate(schema, this.env);
|
|
1293
|
-
for (const key in config) if (typeof config[key] === "string") for (const env in config) config[key] = config[key].replace(new RegExp(`\\$${env}`, "gim"), config[env]);
|
|
1294
|
-
this.cacheEnv.set(schema, config);
|
|
1295
|
-
return config;
|
|
1296
|
-
}
|
|
1297
|
-
/**
|
|
1298
|
-
* Get all environment variable schemas and their parsed values.
|
|
1299
|
-
*
|
|
1300
|
-
* This is useful for DevTools to display all expected environment variables.
|
|
1301
|
-
*/
|
|
1302
|
-
getEnvSchemas() {
|
|
1303
|
-
const result = [];
|
|
1304
|
-
for (const [schema, values] of this.cacheEnv.entries()) result.push({
|
|
1305
|
-
schema,
|
|
1306
|
-
values
|
|
1307
|
-
});
|
|
1308
|
-
return result;
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Dump the current dependency graph of the App.
|
|
1312
|
-
*
|
|
1313
|
-
* This method returns a record where the keys are the names of the services.
|
|
1314
|
-
*/
|
|
1315
|
-
graph() {
|
|
1316
|
-
for (const [key] of this.substitutions.entries()) if (!this.has(key)) this.inject(key);
|
|
1317
|
-
const graph = {};
|
|
1318
|
-
for (const [provide, { parents }] of this.registry.entries()) {
|
|
1319
|
-
graph[provide.name] = { from: parents.filter((it) => !!it).map((it) => it.name) };
|
|
1320
|
-
const aliases = this.substitutions.entries().filter((it) => it[1].use === provide).map((it) => it[0].name).toArray();
|
|
1321
|
-
if (aliases.length) graph[provide.name].as = aliases;
|
|
1322
|
-
const module = Module.of(provide);
|
|
1323
|
-
if (module) graph[provide.name].module = module.name;
|
|
1324
|
-
}
|
|
1325
|
-
return graph;
|
|
1326
|
-
}
|
|
1327
|
-
services(base) {
|
|
1328
|
-
const list = [];
|
|
1329
|
-
for (const [key, value] of this.registry.entries()) if (value.instance instanceof base) list.push(value.instance);
|
|
1330
|
-
return list;
|
|
1331
|
-
}
|
|
1332
|
-
/**
|
|
1333
|
-
* Get all primitives of the specified type.
|
|
1280
|
+
* Get all keys that exist in the state
|
|
1334
1281
|
*/
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
const key1 = factory.toLowerCase().replace("$", "");
|
|
1338
|
-
const key2 = `${key1}primitive`;
|
|
1339
|
-
for (const [key, value] of this.primitiveRegistry.entries()) {
|
|
1340
|
-
const name = key.name.toLowerCase();
|
|
1341
|
-
if (name === key1 || name === key2) return value;
|
|
1342
|
-
}
|
|
1343
|
-
return [];
|
|
1344
|
-
}
|
|
1345
|
-
return this.primitiveRegistry.get(factory[KIND]) ?? [];
|
|
1346
|
-
}
|
|
1347
|
-
new(service, args = []) {
|
|
1348
|
-
this.pendingInstantiations.push(service);
|
|
1349
|
-
__alephaRef.alepha = this;
|
|
1350
|
-
__alephaRef.service = service;
|
|
1351
|
-
const instance = isClass(service) ? new service(...args) : service(...args) ?? {};
|
|
1352
|
-
const obj = instance;
|
|
1353
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
1354
|
-
if (value instanceof Primitive) this.processPrimitive(value, key);
|
|
1355
|
-
if (typeof value === "object" && value !== null && typeof value[OPTIONS] === "object" && "getter" in value[OPTIONS]) {
|
|
1356
|
-
const getter = value[OPTIONS].getter;
|
|
1357
|
-
Object.defineProperty(obj, key, { get: () => this.store.get(getter) });
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
this.pendingInstantiations.pop();
|
|
1361
|
-
if (this.pendingInstantiations.length === 0) __alephaRef.alepha = void 0;
|
|
1362
|
-
__alephaRef.service = this.pendingInstantiations[this.pendingInstantiations.length - 1];
|
|
1363
|
-
return instance;
|
|
1364
|
-
}
|
|
1365
|
-
processPrimitive(value, propertyKey = "") {
|
|
1366
|
-
value.config.propertyKey = propertyKey;
|
|
1367
|
-
value.onInit();
|
|
1368
|
-
const kind = value.constructor;
|
|
1369
|
-
const list = this.primitiveRegistry.get(kind) ?? [];
|
|
1370
|
-
this.primitiveRegistry.set(kind, [...list, value]);
|
|
1371
|
-
}
|
|
1372
|
-
};
|
|
1373
|
-
|
|
1374
|
-
//#endregion
|
|
1375
|
-
//#region ../../src/core/errors/AppNotStartedError.ts
|
|
1376
|
-
var AppNotStartedError = class extends AlephaError {
|
|
1377
|
-
name = "AppNotStartedError";
|
|
1378
|
-
constructor() {
|
|
1379
|
-
super("App not started. Please start the app before.");
|
|
1282
|
+
keys() {
|
|
1283
|
+
return Object.keys(this.store);
|
|
1380
1284
|
}
|
|
1381
1285
|
};
|
|
1382
1286
|
|
|
1383
1287
|
//#endregion
|
|
1384
|
-
//#region ../../src/core/
|
|
1288
|
+
//#region ../../src/core/Alepha.ts
|
|
1385
1289
|
/**
|
|
1386
|
-
*
|
|
1290
|
+
* Core container of the Alepha framework.
|
|
1291
|
+
*
|
|
1292
|
+
* It is responsible for managing the lifecycle of services,
|
|
1293
|
+
* handling dependency injection,
|
|
1294
|
+
* and providing a unified interface for the application.
|
|
1295
|
+
*
|
|
1296
|
+
* @example
|
|
1297
|
+
* ```ts
|
|
1298
|
+
* import { Alepha, run } from "alepha";
|
|
1299
|
+
*
|
|
1300
|
+
* class MyService {
|
|
1301
|
+
* // business logic here
|
|
1302
|
+
* }
|
|
1303
|
+
*
|
|
1304
|
+
* const alepha = Alepha.create({
|
|
1305
|
+
* // state, env, and other properties
|
|
1306
|
+
* })
|
|
1307
|
+
*
|
|
1308
|
+
* alepha.with(MyService);
|
|
1309
|
+
*
|
|
1310
|
+
* run(alepha); // trigger .start (and .stop) automatically
|
|
1311
|
+
* ```
|
|
1312
|
+
*
|
|
1313
|
+
* ### Alepha Factory
|
|
1314
|
+
*
|
|
1315
|
+
* Alepha.create() is an enhanced version of new Alepha().
|
|
1316
|
+
* - It merges `process.env` with the provided state.env when available.
|
|
1317
|
+
* - It populates the test hooks for Vitest or Jest environments when available.
|
|
1387
1318
|
*
|
|
1388
|
-
*
|
|
1389
|
-
* It handles the core pagination logic including:
|
|
1390
|
-
* - Slicing the content to the requested page size
|
|
1391
|
-
* - Calculating pagination metadata (offset, page number, etc.)
|
|
1392
|
-
* - Determining navigation state (isFirst, isLast)
|
|
1393
|
-
* - Including sort metadata when provided
|
|
1319
|
+
* new Alepha() is fine if you don't need these helpers.
|
|
1394
1320
|
*
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1321
|
+
* ### Platforms & Environments
|
|
1322
|
+
*
|
|
1323
|
+
* Alepha is designed to work in various environments:
|
|
1324
|
+
* - **Browser**: Runs in the browser, using the global `window` object.
|
|
1325
|
+
* - **Serverless**: Runs in serverless environments like Vercel or Vite.
|
|
1326
|
+
* - **Test**: Runs in test environments like Jest or Vitest.
|
|
1327
|
+
* - **Production**: Runs in production environments, typically with NODE_ENV set to "production".
|
|
1328
|
+
* * You can check the current environment using the following methods:
|
|
1329
|
+
*
|
|
1330
|
+
* - `isBrowser()`: Returns true if the App is running in a browser environment.
|
|
1331
|
+
* - `isServerless()`: Returns true if the App is running in a serverless environment.
|
|
1332
|
+
* - `isTest()`: Returns true if the App is running in a test environment.
|
|
1333
|
+
* - `isProduction()`: Returns true if the App is running in a production environment.
|
|
1334
|
+
*
|
|
1335
|
+
* ### State & Environment
|
|
1336
|
+
*
|
|
1337
|
+
* The state of the Alepha container is stored in the `store` property.
|
|
1338
|
+
* Most important property is `store.env`, which contains the environment variables.
|
|
1400
1339
|
*
|
|
1401
|
-
* @example Basic pagination
|
|
1402
1340
|
* ```ts
|
|
1403
|
-
* const
|
|
1404
|
-
*
|
|
1405
|
-
* //
|
|
1406
|
-
*
|
|
1341
|
+
* const alepha = Alepha.create({ env: { MY_VAR: "value" } });
|
|
1342
|
+
*
|
|
1343
|
+
* // You can access the environment variables using alepha.env
|
|
1344
|
+
* console.log(alepha.env.MY_VAR); // "value"
|
|
1345
|
+
*
|
|
1346
|
+
* // But you should use $env() primitive to get typed values from the environment.
|
|
1347
|
+
* class App {
|
|
1348
|
+
* env = $env(
|
|
1349
|
+
* t.object({
|
|
1350
|
+
* MY_VAR: t.text(),
|
|
1351
|
+
* })
|
|
1352
|
+
* );
|
|
1353
|
+
* }
|
|
1407
1354
|
* ```
|
|
1408
1355
|
*
|
|
1409
|
-
*
|
|
1356
|
+
* ### Modules
|
|
1357
|
+
*
|
|
1358
|
+
* Modules are a way to group services together.
|
|
1359
|
+
* You can register a module using the `$module` primitive.
|
|
1360
|
+
*
|
|
1410
1361
|
* ```ts
|
|
1411
|
-
*
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1362
|
+
* import { $module } from "alepha";
|
|
1363
|
+
*
|
|
1364
|
+
* class MyLib {}
|
|
1365
|
+
*
|
|
1366
|
+
* const myModule = $module({
|
|
1367
|
+
* name: "my.project.module",
|
|
1368
|
+
* services: [MyLib],
|
|
1369
|
+
* });
|
|
1417
1370
|
* ```
|
|
1418
1371
|
*
|
|
1419
|
-
*
|
|
1372
|
+
* Do not use modules for small applications.
|
|
1373
|
+
*
|
|
1374
|
+
* ### Hooks
|
|
1375
|
+
*
|
|
1376
|
+
* Hooks are a way to run async functions from all registered providers/services.
|
|
1377
|
+
* You can register a hook using the `$hook` primitive.
|
|
1378
|
+
*
|
|
1420
1379
|
* ```ts
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1380
|
+
* import { $hook } from "alepha";
|
|
1381
|
+
*
|
|
1382
|
+
* class App {
|
|
1383
|
+
* log = $logger();
|
|
1384
|
+
* onCustomerHook = $hook({
|
|
1385
|
+
* on: "my:custom:hook",
|
|
1386
|
+
* handler: () => {
|
|
1387
|
+
* this.log?.info("App is being configured");
|
|
1388
|
+
* },
|
|
1389
|
+
* });
|
|
1390
|
+
* }
|
|
1391
|
+
*
|
|
1392
|
+
* Alepha.create()
|
|
1393
|
+
* .with(App)
|
|
1394
|
+
* .start()
|
|
1395
|
+
* .then(alepha => alepha.events.emit("my:custom:hook"));
|
|
1427
1396
|
* ```
|
|
1397
|
+
*
|
|
1398
|
+
* Hooks are fully typed. You can create your own hooks by using module augmentation:
|
|
1399
|
+
*
|
|
1400
|
+
* ```ts
|
|
1401
|
+
* declare module "alepha" {
|
|
1402
|
+
* interface Hooks {
|
|
1403
|
+
* "my:custom:hook": {
|
|
1404
|
+
* arg1: string;
|
|
1405
|
+
* }
|
|
1406
|
+
* }
|
|
1407
|
+
* }
|
|
1408
|
+
* ```
|
|
1409
|
+
*
|
|
1410
|
+
* @module alepha
|
|
1428
1411
|
*/
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
fields: sort.map((s) => ({
|
|
1446
|
-
field: s.column,
|
|
1447
|
-
direction: s.direction
|
|
1448
|
-
}))
|
|
1449
|
-
} } : {}
|
|
1450
|
-
}
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
//#endregion
|
|
1455
|
-
//#region ../../src/core/helpers/FileLike.ts
|
|
1456
|
-
const isTypeFile = (value) => {
|
|
1457
|
-
return value && typeof value === "object" && "format" in value && value.format === "binary";
|
|
1458
|
-
};
|
|
1459
|
-
const isFileLike = (value) => {
|
|
1460
|
-
return !!value && typeof value === "object" && !Array.isArray(value) && typeof value.name === "string" && typeof value.type === "string" && typeof value.size === "number" && typeof value.stream.bind(value) === "function";
|
|
1461
|
-
};
|
|
1462
|
-
|
|
1463
|
-
//#endregion
|
|
1464
|
-
//#region ../../src/core/providers/TypeProvider.ts
|
|
1465
|
-
const isUUID = Format.IsUuid;
|
|
1466
|
-
var TypeGuard = class {
|
|
1467
|
-
isBigInt = (value) => Type.IsString(value) && "format" in value && value.format === "bigint";
|
|
1468
|
-
isUUID = (value) => Type.IsString(value) && "format" in value && value.format === "uuid";
|
|
1469
|
-
isObject = Type.IsObject;
|
|
1470
|
-
isNumber = Type.IsNumber;
|
|
1471
|
-
isString = Type.IsString;
|
|
1472
|
-
isBoolean = Type.IsBoolean;
|
|
1473
|
-
isAny = Type.IsAny;
|
|
1474
|
-
isArray = Type.IsArray;
|
|
1475
|
-
isOptional = Type.IsOptional;
|
|
1476
|
-
isUnion = Type.IsUnion;
|
|
1477
|
-
isInteger = Type.IsInteger;
|
|
1478
|
-
isNull = Type.IsNull;
|
|
1479
|
-
isUndefined = Type.IsUndefined;
|
|
1480
|
-
isUnsafe = Type.IsUnsafe;
|
|
1481
|
-
isRecord = Type.IsRecord;
|
|
1482
|
-
isTuple = Type.IsTuple;
|
|
1483
|
-
isVoid = Type.IsVoid;
|
|
1484
|
-
isLiteral = Type.IsLiteral;
|
|
1485
|
-
isSchema = Type.IsSchema;
|
|
1486
|
-
isFile = isTypeFile;
|
|
1487
|
-
isDateTime = (schema) => {
|
|
1488
|
-
return t.schema.isString(schema) && schema.format === "date-time";
|
|
1489
|
-
};
|
|
1490
|
-
isDate = (schema) => {
|
|
1491
|
-
return t.schema.isString(schema) && schema.format === "date";
|
|
1492
|
-
};
|
|
1493
|
-
isTime = (schema) => {
|
|
1494
|
-
return t.schema.isString(schema) && schema.format === "time";
|
|
1495
|
-
};
|
|
1496
|
-
isDuration = (schema) => {
|
|
1497
|
-
return t.schema.isString(schema) && schema.format === "duration";
|
|
1498
|
-
};
|
|
1499
|
-
};
|
|
1500
|
-
var TypeProvider = class TypeProvider {
|
|
1501
|
-
static format = Format;
|
|
1502
|
-
static {
|
|
1503
|
-
Format.Set("bigint", (value) => TypeProvider.isValidBigInt(value));
|
|
1504
|
-
}
|
|
1505
|
-
static translateError(error, locale) {
|
|
1506
|
-
if (!locale) return error.cause.message;
|
|
1507
|
-
for (const [key, value] of Object.entries(Locale)) {
|
|
1508
|
-
if (key === "Set" || key === "Get" || key === "Reset") continue;
|
|
1509
|
-
if (key === locale || key.startsWith(`${locale}_`)) return value(error.cause);
|
|
1510
|
-
}
|
|
1511
|
-
return error.cause.message;
|
|
1512
|
-
}
|
|
1513
|
-
static setLocale(locale) {
|
|
1514
|
-
for (const [key, value] of Object.entries(Locale)) {
|
|
1515
|
-
if (key === "Set" || key === "Get" || key === "Reset") continue;
|
|
1516
|
-
if (key === locale || key.startsWith(`${locale}_`)) {
|
|
1517
|
-
Locale.Set(value);
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1412
|
+
var Alepha = class Alepha {
|
|
1413
|
+
/**
|
|
1414
|
+
* Creates a new instance of the Alepha container with some helpers:
|
|
1415
|
+
*
|
|
1416
|
+
* - merges `process.env` with the provided state.env when available.
|
|
1417
|
+
* - populates the test hooks for Vitest or Jest environments when available.
|
|
1418
|
+
*
|
|
1419
|
+
* If you are not interested about these helpers, you can use the constructor directly.
|
|
1420
|
+
*/
|
|
1421
|
+
static create(state = {}) {
|
|
1422
|
+
if (typeof process === "object" && typeof process.env === "object") {
|
|
1423
|
+
state.env = {
|
|
1424
|
+
...state.env,
|
|
1425
|
+
...process.env
|
|
1426
|
+
};
|
|
1427
|
+
if (process.env.NODE_ENV === "production") state.env.NODE_ENV ??= "production";
|
|
1520
1428
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1429
|
+
const alepha = new Alepha(state);
|
|
1430
|
+
if (alepha.isTest()) {
|
|
1431
|
+
const g = globalThis;
|
|
1432
|
+
const beforeAll = state["alepha.test.beforeAll"] ?? g.beforeAll;
|
|
1433
|
+
const afterAll = state["alepha.test.afterAll"] ?? g.afterAll;
|
|
1434
|
+
const afterEach = state["alepha.test.afterEach"] ?? g.afterEach;
|
|
1435
|
+
const onTestFinished = state["alepha.test.onTestFinished"] ?? g.onTestFinished;
|
|
1436
|
+
beforeAll?.(() => alepha.start());
|
|
1437
|
+
afterAll?.(() => alepha.stop());
|
|
1438
|
+
try {
|
|
1439
|
+
onTestFinished?.(() => alepha.stop());
|
|
1440
|
+
} catch (_error) {}
|
|
1441
|
+
alepha.store.set("alepha.test.beforeAll", beforeAll).set("alepha.test.afterAll", afterAll).set("alepha.test.afterEach", afterEach).set("alepha.test.onTestFinished", onTestFinished);
|
|
1532
1442
|
}
|
|
1443
|
+
return alepha;
|
|
1533
1444
|
}
|
|
1534
1445
|
/**
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
*
|
|
1541
|
-
|
|
1446
|
+
* Flag indicating whether the App won't accept any further changes.
|
|
1447
|
+
* Pass to true when #start() is called.
|
|
1448
|
+
*/
|
|
1449
|
+
locked = false;
|
|
1450
|
+
/**
|
|
1451
|
+
* True if the App has been configured.
|
|
1452
|
+
*/
|
|
1453
|
+
configured = false;
|
|
1454
|
+
/**
|
|
1455
|
+
* True if the App has started.
|
|
1456
|
+
*/
|
|
1457
|
+
started = false;
|
|
1458
|
+
/**
|
|
1459
|
+
* True if the App is ready.
|
|
1542
1460
|
*/
|
|
1543
|
-
|
|
1461
|
+
ready = false;
|
|
1544
1462
|
/**
|
|
1545
|
-
*
|
|
1463
|
+
* A promise that resolves when the App has started.
|
|
1546
1464
|
*/
|
|
1547
|
-
|
|
1465
|
+
starting;
|
|
1548
1466
|
/**
|
|
1549
|
-
*
|
|
1550
|
-
* It
|
|
1551
|
-
*
|
|
1552
|
-
* It can be set to a larger value:
|
|
1553
|
-
* ```ts
|
|
1554
|
-
* TypeProvider.DEFAULT_LONG_STRING_MAX_LENGTH = 2048;
|
|
1555
|
-
* ```
|
|
1467
|
+
* During the instantiation process, we keep a list of pending instantiations.
|
|
1468
|
+
* > It allows us to detect circular dependencies.
|
|
1556
1469
|
*/
|
|
1557
|
-
|
|
1470
|
+
pendingInstantiations = [];
|
|
1558
1471
|
/**
|
|
1559
|
-
*
|
|
1560
|
-
*
|
|
1561
|
-
|
|
1472
|
+
* Cache for environment variables.
|
|
1473
|
+
* > It allows us to avoid parsing the same schema multiple times.
|
|
1474
|
+
*/
|
|
1475
|
+
cacheEnv = /* @__PURE__ */ new Map();
|
|
1476
|
+
/**
|
|
1477
|
+
* List of modules that are registered in the container.
|
|
1562
1478
|
*
|
|
1563
|
-
*
|
|
1479
|
+
* Modules are used to group services and provide a way to register them in the container.
|
|
1480
|
+
*/
|
|
1481
|
+
modules = [];
|
|
1482
|
+
/**
|
|
1483
|
+
* List of service substitutions.
|
|
1564
1484
|
*
|
|
1565
|
-
*
|
|
1566
|
-
* ```ts
|
|
1567
|
-
* TypeProvider.DEFAULT_RICH_STRING_MAX_LENGTH = 1000000;
|
|
1568
|
-
* ```
|
|
1485
|
+
* Services registered here will be replaced by the specified service when injected.
|
|
1569
1486
|
*/
|
|
1570
|
-
|
|
1487
|
+
substitutions = /* @__PURE__ */ new Map();
|
|
1571
1488
|
/**
|
|
1572
|
-
*
|
|
1573
|
-
* This is a default value to prevent excessive memory usage.
|
|
1574
|
-
* It can be overridden in the array options.
|
|
1489
|
+
* Registry of primitives.
|
|
1575
1490
|
*/
|
|
1576
|
-
|
|
1577
|
-
raw = Type;
|
|
1578
|
-
any = Type.Any;
|
|
1579
|
-
void = Type.Void;
|
|
1580
|
-
undefined = Type.Undefined;
|
|
1581
|
-
record = Type.Record;
|
|
1582
|
-
union = Type.Union;
|
|
1583
|
-
tuple = Type.Tuple;
|
|
1584
|
-
null = Type.Null;
|
|
1585
|
-
const = Type.Literal;
|
|
1586
|
-
options = Type.Options;
|
|
1491
|
+
primitiveRegistry = /* @__PURE__ */ new Map();
|
|
1587
1492
|
/**
|
|
1588
|
-
*
|
|
1589
|
-
* This is not a runtime type check, but a compile-time type guard.
|
|
1590
|
-
*
|
|
1591
|
-
* @example
|
|
1592
|
-
* ```ts
|
|
1593
|
-
* if (t.schema.isString(schema)) {
|
|
1594
|
-
* // schema is TString
|
|
1595
|
-
* }
|
|
1596
|
-
* ```
|
|
1493
|
+
* List of all services + how they are provided.
|
|
1597
1494
|
*/
|
|
1598
|
-
|
|
1599
|
-
extend(schema, properties, options) {
|
|
1600
|
-
return Type.Interface(Array.isArray(schema) ? schema : [schema], properties, {
|
|
1601
|
-
additionalProperties: false,
|
|
1602
|
-
...options
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
pick(schema, keys, options) {
|
|
1606
|
-
return Type.Pick(schema, keys, {
|
|
1607
|
-
additionalProperties: false,
|
|
1608
|
-
...options
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1611
|
-
omit(schema, keys, options) {
|
|
1612
|
-
return Type.Omit(schema, keys, {
|
|
1613
|
-
additionalProperties: false,
|
|
1614
|
-
...options
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
1617
|
-
partial(schema, options) {
|
|
1618
|
-
return Type.Partial(schema, {
|
|
1619
|
-
additionalProperties: false,
|
|
1620
|
-
...options
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1495
|
+
registry = /* @__PURE__ */ new Map();
|
|
1623
1496
|
/**
|
|
1624
|
-
*
|
|
1625
|
-
* By default, additional properties are not allowed.
|
|
1497
|
+
* Node.js feature that allows to store context across asynchronous calls.
|
|
1626
1498
|
*
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
* id: t.integer(),
|
|
1631
|
-
* name: t.string(),
|
|
1632
|
-
* });
|
|
1633
|
-
* ```
|
|
1499
|
+
* This is used for logging, tracing, and other context-related features.
|
|
1500
|
+
*
|
|
1501
|
+
* Mocked for browser environments.
|
|
1634
1502
|
*/
|
|
1635
|
-
|
|
1636
|
-
return Type.Object(properties, {
|
|
1637
|
-
additionalProperties: false,
|
|
1638
|
-
...options
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1503
|
+
context;
|
|
1641
1504
|
/**
|
|
1642
|
-
*
|
|
1643
|
-
|
|
1505
|
+
* Event manager to handle lifecycle events and custom events.
|
|
1506
|
+
*/
|
|
1507
|
+
events;
|
|
1508
|
+
/**
|
|
1509
|
+
* State manager to store arbitrary values.
|
|
1510
|
+
*/
|
|
1511
|
+
store;
|
|
1512
|
+
/**
|
|
1513
|
+
* Codec manager for encoding and decoding data with different formats.
|
|
1644
1514
|
*
|
|
1645
|
-
*
|
|
1515
|
+
* Supports multiple codec formats (JSON, Protobuf, etc.) with a unified interface.
|
|
1646
1516
|
*/
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1517
|
+
codec;
|
|
1518
|
+
/**
|
|
1519
|
+
* Get logger instance.
|
|
1520
|
+
*/
|
|
1521
|
+
get log() {
|
|
1522
|
+
return this.store.get("alepha.logger");
|
|
1652
1523
|
}
|
|
1653
1524
|
/**
|
|
1654
|
-
*
|
|
1655
|
-
* For db or input fields, consider using `t.text()` instead, which has length limits.
|
|
1656
|
-
*
|
|
1657
|
-
* If you need a string with specific format (e.g. email, uuid), consider using the corresponding method (e.g. `t.email()`, `t.uuid()`).
|
|
1525
|
+
* The environment variables for the App.
|
|
1658
1526
|
*/
|
|
1659
|
-
|
|
1660
|
-
return
|
|
1527
|
+
get env() {
|
|
1528
|
+
return this.store.get("env") ?? {};
|
|
1529
|
+
}
|
|
1530
|
+
constructor(state = {}) {
|
|
1531
|
+
this.store = this.inject(StateManager, { args: [state] });
|
|
1532
|
+
this.events = this.inject(EventManager);
|
|
1533
|
+
this.events.logFn = () => this.log;
|
|
1534
|
+
this.context = this.inject(AlsProvider);
|
|
1535
|
+
this.codec = this.inject(CodecManager);
|
|
1536
|
+
}
|
|
1537
|
+
set(target, value) {
|
|
1538
|
+
this.store.set(target, value);
|
|
1539
|
+
return this;
|
|
1661
1540
|
}
|
|
1662
1541
|
/**
|
|
1663
|
-
*
|
|
1664
|
-
* For internal strings without length limits, consider using `t.string()` instead.
|
|
1542
|
+
* True when start() is called.
|
|
1665
1543
|
*
|
|
1666
|
-
*
|
|
1544
|
+
* -> No more services can be added, it's over, bye!
|
|
1667
1545
|
*/
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
const maxLength = size === "short" ? TypeProvider.DEFAULT_SHORT_STRING_MAX_LENGTH : size === "long" ? TypeProvider.DEFAULT_LONG_STRING_MAX_LENGTH : size === "rich" ? TypeProvider.DEFAULT_RICH_STRING_MAX_LENGTH : TypeProvider.DEFAULT_STRING_MAX_LENGTH;
|
|
1671
|
-
return Type.String({
|
|
1672
|
-
maxLength,
|
|
1673
|
-
"~options": {
|
|
1674
|
-
trim: options.trim ?? true,
|
|
1675
|
-
lowercase: options.lowercase ?? false
|
|
1676
|
-
},
|
|
1677
|
-
...rest
|
|
1678
|
-
});
|
|
1546
|
+
isLocked() {
|
|
1547
|
+
return this.locked;
|
|
1679
1548
|
}
|
|
1680
1549
|
/**
|
|
1681
|
-
*
|
|
1682
|
-
*
|
|
1550
|
+
* Returns whether the App is configured.
|
|
1551
|
+
*
|
|
1552
|
+
* It means that Alepha#configure() has been called.
|
|
1553
|
+
*
|
|
1554
|
+
* > By default, configure() is called automatically when start() is called, but you can also call it manually.
|
|
1683
1555
|
*/
|
|
1684
|
-
|
|
1685
|
-
return
|
|
1556
|
+
isConfigured() {
|
|
1557
|
+
return this.configured;
|
|
1686
1558
|
}
|
|
1687
1559
|
/**
|
|
1688
|
-
*
|
|
1560
|
+
* Returns whether the App has started.
|
|
1561
|
+
*
|
|
1562
|
+
* It means that #start() has been called but maybe not all services are ready.
|
|
1689
1563
|
*/
|
|
1690
|
-
|
|
1691
|
-
return
|
|
1564
|
+
isStarted() {
|
|
1565
|
+
return this.started;
|
|
1692
1566
|
}
|
|
1693
1567
|
/**
|
|
1694
|
-
*
|
|
1568
|
+
* True if the App is ready. It means that Alepha is started AND ready() hook has beed called.
|
|
1695
1569
|
*/
|
|
1696
|
-
|
|
1697
|
-
return
|
|
1570
|
+
isReady() {
|
|
1571
|
+
return this.ready;
|
|
1698
1572
|
}
|
|
1699
1573
|
/**
|
|
1700
|
-
*
|
|
1574
|
+
* True if the App is running in a Continuous Integration environment.
|
|
1701
1575
|
*/
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
int32(options) {
|
|
1706
|
-
return Type.Integer({
|
|
1707
|
-
minimum: -2147483647,
|
|
1708
|
-
maximum: 2147483647,
|
|
1709
|
-
...options
|
|
1710
|
-
});
|
|
1576
|
+
isCI() {
|
|
1577
|
+
if (this.env.GITHUB_ACTIONS) return true;
|
|
1578
|
+
return !!this.env.CI;
|
|
1711
1579
|
}
|
|
1712
1580
|
/**
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1715
|
-
* This is NOT a true int64, as JavaScript cannot represent all int64 values.
|
|
1716
|
-
* It is a number that is an integer and between -9007199254740991 and 9007199254740991.
|
|
1717
|
-
* Use `t.bigint()` for true int64 values represented as strings.
|
|
1581
|
+
* True if the App is running in a browser environment.
|
|
1718
1582
|
*/
|
|
1719
|
-
|
|
1720
|
-
return
|
|
1721
|
-
format: "int64",
|
|
1722
|
-
multipleOf: 1,
|
|
1723
|
-
minimum: -9007199254740991,
|
|
1724
|
-
maximum: 9007199254740991,
|
|
1725
|
-
...options
|
|
1726
|
-
});
|
|
1583
|
+
isBrowser() {
|
|
1584
|
+
return typeof window !== "undefined";
|
|
1727
1585
|
}
|
|
1728
1586
|
/**
|
|
1729
|
-
*
|
|
1587
|
+
* Returns whether the App is running in Vite dev mode.
|
|
1730
1588
|
*/
|
|
1731
|
-
|
|
1732
|
-
|
|
1589
|
+
isViteDev() {
|
|
1590
|
+
if (this.isBrowser()) return false;
|
|
1591
|
+
return !!this.env.VITE_ALEPHA_DEV;
|
|
1592
|
+
}
|
|
1593
|
+
isBun() {
|
|
1594
|
+
return "Bun" in globalThis;
|
|
1733
1595
|
}
|
|
1734
1596
|
/**
|
|
1735
|
-
*
|
|
1597
|
+
* Returns whether the App is running in a serverless environment.
|
|
1736
1598
|
*/
|
|
1737
|
-
|
|
1738
|
-
|
|
1599
|
+
isServerless() {
|
|
1600
|
+
if (this.isBrowser()) return false;
|
|
1601
|
+
if (this.env.VERCEL_REGION) return true;
|
|
1602
|
+
if (typeof global === "object" && typeof global.Cloudflare === "object") return true;
|
|
1603
|
+
return false;
|
|
1739
1604
|
}
|
|
1740
1605
|
/**
|
|
1741
|
-
*
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
/**
|
|
1745
|
-
* Create a schema for a string enum.
|
|
1606
|
+
* Returns whether the App is in test mode. (Running in a test environment)
|
|
1607
|
+
*
|
|
1608
|
+
* > This is automatically set when running tests with Jest or Vitest.
|
|
1746
1609
|
*/
|
|
1747
|
-
|
|
1748
|
-
return
|
|
1749
|
-
enum: values,
|
|
1750
|
-
pattern: values.map((v) => `^${v}$`).join("|"),
|
|
1751
|
-
...options
|
|
1752
|
-
}));
|
|
1610
|
+
isTest() {
|
|
1611
|
+
return this.env.NODE_ENV === "test";
|
|
1753
1612
|
}
|
|
1754
1613
|
/**
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1614
|
+
* Returns whether the App is in production mode. (Running in a production environment)
|
|
1615
|
+
*
|
|
1616
|
+
* > This is automatically set by Vite or Vercel. However, you have to set it manually when running Docker apps.
|
|
1757
1617
|
*/
|
|
1758
|
-
|
|
1759
|
-
return
|
|
1760
|
-
...options,
|
|
1761
|
-
format: "bigint"
|
|
1762
|
-
});
|
|
1618
|
+
isProduction() {
|
|
1619
|
+
return this.env.NODE_ENV === "production";
|
|
1763
1620
|
}
|
|
1764
1621
|
/**
|
|
1765
|
-
*
|
|
1622
|
+
* Starts the App.
|
|
1623
|
+
*
|
|
1624
|
+
* - Lock any further changes to the container.
|
|
1625
|
+
* - Run "configure" hook for all services. Primitives will be processed.
|
|
1626
|
+
* - Run "start" hook for all services. Providers will connect/listen/...
|
|
1627
|
+
* - Run "ready" hook for all services. This is the point where the App is ready to serve requests.
|
|
1628
|
+
*
|
|
1629
|
+
* @return A promise that resolves when the App has started.
|
|
1766
1630
|
*/
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
...
|
|
1770
|
-
|
|
1771
|
-
}
|
|
1631
|
+
async start() {
|
|
1632
|
+
if (this.ready) {
|
|
1633
|
+
this.log?.debug("App is already started, skipping...");
|
|
1634
|
+
return this;
|
|
1635
|
+
}
|
|
1636
|
+
if (this.starting) {
|
|
1637
|
+
this.log?.warn("App is already starting, waiting for it to finish...");
|
|
1638
|
+
return this.starting.promise;
|
|
1639
|
+
}
|
|
1640
|
+
this.starting = Promise.withResolvers();
|
|
1641
|
+
const now = Date.now();
|
|
1642
|
+
this.log?.info("Starting App...");
|
|
1643
|
+
for (const [key] of this.substitutions.entries()) this.inject(key);
|
|
1644
|
+
const target = this.store.get("alepha.target");
|
|
1645
|
+
if (target) {
|
|
1646
|
+
this.registry = /* @__PURE__ */ new Map();
|
|
1647
|
+
this.primitiveRegistry = /* @__PURE__ */ new Map();
|
|
1648
|
+
this.with(target);
|
|
1649
|
+
}
|
|
1650
|
+
this.locked = true;
|
|
1651
|
+
await this.events.emit("configure", this, { log: true });
|
|
1652
|
+
this.configured = true;
|
|
1653
|
+
await this.events.emit("start", this, { log: true });
|
|
1654
|
+
this.started = true;
|
|
1655
|
+
await this.events.emit("ready", this, { log: true });
|
|
1656
|
+
this.log?.info(`App is now ready [${Date.now() - now}ms]`);
|
|
1657
|
+
this.ready = true;
|
|
1658
|
+
this.starting.resolve(this);
|
|
1659
|
+
this.starting = void 0;
|
|
1660
|
+
return this;
|
|
1772
1661
|
}
|
|
1773
1662
|
/**
|
|
1774
|
-
*
|
|
1663
|
+
* Stops the App.
|
|
1664
|
+
*
|
|
1665
|
+
* - Run "stop" hook for all services.
|
|
1666
|
+
*
|
|
1667
|
+
* Stop will NOT reset the container.
|
|
1668
|
+
* Stop will NOT unlock the container.
|
|
1669
|
+
*
|
|
1670
|
+
* > Stop is used to gracefully shut down the application, nothing more. There is no "restart".
|
|
1671
|
+
*
|
|
1672
|
+
* @return A promise that resolves when the App has stopped.
|
|
1775
1673
|
*/
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1674
|
+
async stop() {
|
|
1675
|
+
if (!this.started) return;
|
|
1676
|
+
this.log?.info("Stopping App...");
|
|
1677
|
+
await this.events.emit("stop", this, {
|
|
1678
|
+
reverse: true,
|
|
1679
|
+
log: true
|
|
1780
1680
|
});
|
|
1681
|
+
this.log?.info("App is now off");
|
|
1682
|
+
this.started = false;
|
|
1683
|
+
this.ready = false;
|
|
1781
1684
|
}
|
|
1782
1685
|
/**
|
|
1783
|
-
*
|
|
1686
|
+
* Check if entry is registered in the container.
|
|
1784
1687
|
*/
|
|
1785
|
-
|
|
1786
|
-
return
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1688
|
+
has(entry, opts = {}) {
|
|
1689
|
+
if (entry === Alepha) return true;
|
|
1690
|
+
const { inStack = true, inRegistry = true, inSubstitutions = true, registry = this.registry } = opts;
|
|
1691
|
+
const { provide } = typeof entry === "object" && "provide" in entry ? entry : { provide: entry };
|
|
1692
|
+
if (inSubstitutions) {
|
|
1693
|
+
if (this.substitutions.get(provide)) return true;
|
|
1694
|
+
}
|
|
1695
|
+
if (inRegistry) {
|
|
1696
|
+
if (registry.get(provide)) return true;
|
|
1697
|
+
}
|
|
1698
|
+
if (inStack) {
|
|
1699
|
+
const substitute = this.substitutions.get(provide)?.use;
|
|
1700
|
+
if (substitute && this.pendingInstantiations.includes(substitute)) return true;
|
|
1701
|
+
return this.pendingInstantiations.includes(provide);
|
|
1702
|
+
}
|
|
1703
|
+
return false;
|
|
1790
1704
|
}
|
|
1791
1705
|
/**
|
|
1792
|
-
*
|
|
1706
|
+
* Registers the specified service in the container.
|
|
1793
1707
|
*
|
|
1794
|
-
*
|
|
1708
|
+
* - If the service is ALREADY registered, the method does nothing.
|
|
1709
|
+
* - If the service is NOT registered, a new instance is created and registered.
|
|
1795
1710
|
*
|
|
1796
|
-
*
|
|
1711
|
+
* Method is chainable, so you can register multiple services in a single call.
|
|
1712
|
+
*
|
|
1713
|
+
* > ServiceEntry allows to provide a service **substitution** feature.
|
|
1714
|
+
*
|
|
1715
|
+
* @example
|
|
1716
|
+
* ```ts
|
|
1717
|
+
* class A { value = "a"; }
|
|
1718
|
+
* class B { value = "b"; }
|
|
1719
|
+
* class M { a = $inject(A); }
|
|
1720
|
+
*
|
|
1721
|
+
* Alepha.create().with({ provide: A, use: B }).get(M).a.value; // "b"
|
|
1722
|
+
* ```
|
|
1723
|
+
*
|
|
1724
|
+
* > **Substitution** is an advanced feature that allows you to replace a service with another service.
|
|
1725
|
+
* > It's useful for testing or for providing different implementations of a service.
|
|
1726
|
+
* > If you are interested in configuring a service, use Alepha#configure() instead.
|
|
1727
|
+
*
|
|
1728
|
+
* @param serviceEntry - The service to register in the container.
|
|
1729
|
+
* @return Current instance of Alepha.
|
|
1797
1730
|
*/
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1731
|
+
with(serviceEntry) {
|
|
1732
|
+
const entry = "default" in serviceEntry ? serviceEntry.default : serviceEntry;
|
|
1733
|
+
if (this.has(entry, {
|
|
1734
|
+
inSubstitutions: false,
|
|
1735
|
+
inRegistry: false
|
|
1736
|
+
})) return this;
|
|
1737
|
+
if (typeof entry === "object") {
|
|
1738
|
+
if (entry.provide === entry.use) {
|
|
1739
|
+
this.inject(entry.provide);
|
|
1740
|
+
return this;
|
|
1741
|
+
}
|
|
1742
|
+
if (!this.substitutions.has(entry.provide) && !this.has(entry.provide)) {
|
|
1743
|
+
if (this.started) throw new ContainerLockedError();
|
|
1744
|
+
if (MODULE in entry.provide && typeof entry.provide[MODULE] === "function") entry.use[MODULE] ??= entry.provide[MODULE];
|
|
1745
|
+
this.substitutions.set(entry.provide, { use: entry.use });
|
|
1746
|
+
} else if (!entry.optional) throw new TooLateSubstitutionError(entry.provide.name, entry.use.name);
|
|
1747
|
+
return this;
|
|
1748
|
+
}
|
|
1749
|
+
this.inject(entry);
|
|
1750
|
+
return this;
|
|
1803
1751
|
}
|
|
1804
1752
|
/**
|
|
1805
|
-
*
|
|
1753
|
+
* Get an instance of the specified service from the container.
|
|
1754
|
+
*
|
|
1755
|
+
* @see {@link InjectOptions} for the available options.
|
|
1806
1756
|
*/
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
e164(options) {
|
|
1822
|
-
return this.text({
|
|
1823
|
-
...options,
|
|
1824
|
-
description: "Phone number in E.164 format, e.g. +1234567890.",
|
|
1825
|
-
pattern: "^\\+[1-9]\\d{1,14}$"
|
|
1826
|
-
});
|
|
1827
|
-
}
|
|
1828
|
-
bcp47(options) {
|
|
1829
|
-
return this.text({
|
|
1830
|
-
...options,
|
|
1831
|
-
description: "BCP 47 language tag, e.g. en, en-US, fr, fr-CA.",
|
|
1832
|
-
pattern: "^[a-z]{2,3}(?:-[A-Z]{2})?$"
|
|
1757
|
+
inject(service, opts = {}) {
|
|
1758
|
+
const lifetime = opts.lifetime ?? "singleton";
|
|
1759
|
+
const parent = opts.parent !== void 0 ? opts.parent : __alephaRef?.parent ?? Alepha;
|
|
1760
|
+
const transient = lifetime === "transient";
|
|
1761
|
+
const registry = lifetime === "scoped" ? this.context.get("registry") ?? this.registry : this.registry;
|
|
1762
|
+
if (service === Alepha) return this;
|
|
1763
|
+
if (typeof service === "string") {
|
|
1764
|
+
for (const [key, value] of registry.entries()) if (key.name === service) return value.instance;
|
|
1765
|
+
throw new AlephaError(`Service not found: ${service}`);
|
|
1766
|
+
}
|
|
1767
|
+
const substitute = this.substitutions.get(service);
|
|
1768
|
+
if (substitute) return this.inject(substitute.use, {
|
|
1769
|
+
parent,
|
|
1770
|
+
lifetime
|
|
1833
1771
|
});
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1772
|
+
const index = this.pendingInstantiations.indexOf(service);
|
|
1773
|
+
if (index !== -1 && !transient) throw new CircularDependencyError(service.name, this.pendingInstantiations.slice(0, index).map((it) => it.name));
|
|
1774
|
+
if (!transient) {
|
|
1775
|
+
const match = registry.get(service);
|
|
1776
|
+
if (match) {
|
|
1777
|
+
if (!match.parents.includes(parent) && parent !== service) match.parents.push(parent);
|
|
1778
|
+
return match.instance;
|
|
1779
|
+
}
|
|
1780
|
+
if (this.started) throw new ContainerLockedError(`Container is locked. No more services can be added. ${parent?.name} -> ${service.name}`);
|
|
1781
|
+
}
|
|
1782
|
+
const module = service[MODULE];
|
|
1783
|
+
if (module && typeof module === "function") this.with(module);
|
|
1784
|
+
if (this.has(service, { registry }) && !transient) return this.inject(service, {
|
|
1785
|
+
parent,
|
|
1786
|
+
lifetime
|
|
1843
1787
|
});
|
|
1788
|
+
const instance = this.new(service, opts.args);
|
|
1789
|
+
const definition = {
|
|
1790
|
+
parents: [parent],
|
|
1791
|
+
instance
|
|
1792
|
+
};
|
|
1793
|
+
if (!transient) registry.set(service, definition);
|
|
1794
|
+
if (instance instanceof Module) {
|
|
1795
|
+
this.modules.push(instance);
|
|
1796
|
+
const parent$1 = __alephaRef.parent;
|
|
1797
|
+
__alephaRef.parent = instance.constructor;
|
|
1798
|
+
instance.register(this);
|
|
1799
|
+
__alephaRef.parent = parent$1;
|
|
1800
|
+
}
|
|
1801
|
+
return instance;
|
|
1844
1802
|
}
|
|
1845
1803
|
/**
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1804
|
+
* Applies environment variables to the provided schema and state object.
|
|
1805
|
+
*
|
|
1806
|
+
* It replaces also all templated $ENV inside string values.
|
|
1807
|
+
*
|
|
1808
|
+
* @param schema - The schema object to apply environment variables to.
|
|
1809
|
+
* @return The schema object with environment variables applied.
|
|
1848
1810
|
*/
|
|
1849
|
-
|
|
1850
|
-
return this.
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1811
|
+
parseEnv(schema) {
|
|
1812
|
+
if (this.cacheEnv.has(schema)) return this.cacheEnv.get(schema);
|
|
1813
|
+
const config = this.codec.validate(schema, this.env);
|
|
1814
|
+
for (const key in config) if (typeof config[key] === "string") for (const env in config) config[key] = config[key].replace(new RegExp(`\\$${env}`, "gim"), config[env]);
|
|
1815
|
+
this.cacheEnv.set(schema, config);
|
|
1816
|
+
return config;
|
|
1854
1817
|
}
|
|
1855
1818
|
/**
|
|
1856
|
-
*
|
|
1857
|
-
*
|
|
1819
|
+
* Get all environment variable schemas and their parsed values.
|
|
1820
|
+
*
|
|
1821
|
+
* This is useful for DevTools to display all expected environment variables.
|
|
1858
1822
|
*/
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1823
|
+
getEnvSchemas() {
|
|
1824
|
+
const result = [];
|
|
1825
|
+
for (const [schema, values] of this.cacheEnv.entries()) result.push({
|
|
1826
|
+
schema,
|
|
1827
|
+
values
|
|
1863
1828
|
});
|
|
1829
|
+
return result;
|
|
1864
1830
|
}
|
|
1865
1831
|
/**
|
|
1866
|
-
*
|
|
1832
|
+
* Dump the current dependency graph of the App.
|
|
1833
|
+
*
|
|
1834
|
+
* This method returns a record where the keys are the names of the services.
|
|
1867
1835
|
*/
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1836
|
+
graph() {
|
|
1837
|
+
for (const [key] of this.substitutions.entries()) if (!this.has(key)) this.inject(key);
|
|
1838
|
+
const graph = {};
|
|
1839
|
+
for (const [provide, { parents }] of this.registry.entries()) {
|
|
1840
|
+
graph[provide.name] = { from: parents.filter((it) => !!it).map((it) => it.name) };
|
|
1841
|
+
const aliases = this.substitutions.entries().filter((it) => it[1].use === provide).map((it) => it[0].name).toArray();
|
|
1842
|
+
if (aliases.length) graph[provide.name].as = aliases;
|
|
1843
|
+
const module = Module.of(provide);
|
|
1844
|
+
if (module) graph[provide.name].module = module.name;
|
|
1845
|
+
}
|
|
1846
|
+
return graph;
|
|
1847
|
+
}
|
|
1848
|
+
services(base) {
|
|
1849
|
+
const list = [];
|
|
1850
|
+
for (const [key, value] of this.registry.entries()) if (value.instance instanceof base) list.push(value.instance);
|
|
1851
|
+
return list;
|
|
1852
|
+
}
|
|
1872
1853
|
/**
|
|
1873
|
-
*
|
|
1854
|
+
* Get all primitives of the specified type.
|
|
1874
1855
|
*/
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1856
|
+
primitives(factory) {
|
|
1857
|
+
if (typeof factory === "string") {
|
|
1858
|
+
const key1 = factory.toLowerCase().replace("$", "");
|
|
1859
|
+
const key2 = `${key1}primitive`;
|
|
1860
|
+
for (const [key, value] of this.primitiveRegistry.entries()) {
|
|
1861
|
+
const name = key.name.toLowerCase();
|
|
1862
|
+
if (name === key1 || name === key2) return value;
|
|
1863
|
+
}
|
|
1864
|
+
return [];
|
|
1865
|
+
}
|
|
1866
|
+
return this.primitiveRegistry.get(factory[KIND]) ?? [];
|
|
1867
|
+
}
|
|
1868
|
+
new(service, args = []) {
|
|
1869
|
+
this.pendingInstantiations.push(service);
|
|
1870
|
+
__alephaRef.alepha = this;
|
|
1871
|
+
__alephaRef.service = service;
|
|
1872
|
+
const instance = isClass(service) ? new service(...args) : service(...args) ?? {};
|
|
1873
|
+
const obj = instance;
|
|
1874
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1875
|
+
if (value instanceof Primitive) this.processPrimitive(value, key);
|
|
1876
|
+
if (typeof value === "object" && value !== null && typeof value[OPTIONS] === "object" && "getter" in value[OPTIONS]) {
|
|
1877
|
+
const getter = value[OPTIONS].getter;
|
|
1878
|
+
Object.defineProperty(obj, key, { get: () => this.store.get(getter) });
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
this.pendingInstantiations.pop();
|
|
1882
|
+
if (this.pendingInstantiations.length === 0) __alephaRef.alepha = void 0;
|
|
1883
|
+
__alephaRef.service = this.pendingInstantiations[this.pendingInstantiations.length - 1];
|
|
1884
|
+
return instance;
|
|
1885
|
+
}
|
|
1886
|
+
processPrimitive(value, propertyKey = "") {
|
|
1887
|
+
value.config.propertyKey = propertyKey;
|
|
1888
|
+
value.onInit();
|
|
1889
|
+
const kind = value.constructor;
|
|
1890
|
+
const list = this.primitiveRegistry.get(kind) ?? [];
|
|
1891
|
+
this.primitiveRegistry.set(kind, [...list, value]);
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
//#endregion
|
|
1896
|
+
//#region ../../src/core/errors/AppNotStartedError.ts
|
|
1897
|
+
var AppNotStartedError = class extends AlephaError {
|
|
1898
|
+
name = "AppNotStartedError";
|
|
1899
|
+
constructor() {
|
|
1900
|
+
super("App not started. Please start the app before.");
|
|
1901
|
+
}
|
|
1899
1902
|
};
|
|
1900
|
-
|
|
1903
|
+
|
|
1904
|
+
//#endregion
|
|
1905
|
+
//#region ../../src/core/helpers/createPagination.ts
|
|
1906
|
+
/**
|
|
1907
|
+
* Create a pagination object from an array of entities.
|
|
1908
|
+
*
|
|
1909
|
+
* This is a pure function that works with any data source (databases, APIs, caches, arrays, etc.).
|
|
1910
|
+
* It handles the core pagination logic including:
|
|
1911
|
+
* - Slicing the content to the requested page size
|
|
1912
|
+
* - Calculating pagination metadata (offset, page number, etc.)
|
|
1913
|
+
* - Determining navigation state (isFirst, isLast)
|
|
1914
|
+
* - Including sort metadata when provided
|
|
1915
|
+
*
|
|
1916
|
+
* @param entities - The entities to paginate (should include one extra item to detect if there's a next page)
|
|
1917
|
+
* @param limit - The limit of the pagination (page size)
|
|
1918
|
+
* @param offset - The offset of the pagination (starting position)
|
|
1919
|
+
* @param sort - Optional sort metadata to include in response
|
|
1920
|
+
* @returns A complete Page object with content and metadata
|
|
1921
|
+
*
|
|
1922
|
+
* @example Basic pagination
|
|
1923
|
+
* ```ts
|
|
1924
|
+
* const users = await fetchUsers({ limit: 11, offset: 0 }); // Fetch limit + 1
|
|
1925
|
+
* const page = createPagination(users, 10, 0);
|
|
1926
|
+
* // page.content has max 10 items
|
|
1927
|
+
* // page.page.isLast tells us if there are more pages
|
|
1928
|
+
* ```
|
|
1929
|
+
*
|
|
1930
|
+
* @example With sorting
|
|
1931
|
+
* ```ts
|
|
1932
|
+
* const page = createPagination(
|
|
1933
|
+
* entities,
|
|
1934
|
+
* 10,
|
|
1935
|
+
* 0,
|
|
1936
|
+
* [{ column: "name", direction: "asc" }]
|
|
1937
|
+
* );
|
|
1938
|
+
* ```
|
|
1939
|
+
*
|
|
1940
|
+
* @example In a custom service
|
|
1941
|
+
* ```ts
|
|
1942
|
+
* class MyService {
|
|
1943
|
+
* async listItems(page: number, size: number) {
|
|
1944
|
+
* const items = await this.fetchItems({ limit: size + 1, offset: page * size });
|
|
1945
|
+
* return createPagination(items, size, page * size);
|
|
1946
|
+
* }
|
|
1947
|
+
* }
|
|
1948
|
+
* ```
|
|
1949
|
+
*/
|
|
1950
|
+
function createPagination(entities, limit = 10, offset = 0, sort) {
|
|
1951
|
+
const content = entities.slice(0, limit);
|
|
1952
|
+
const hasNext = entities.length === limit + 1;
|
|
1953
|
+
const pageNumber = Math.floor(offset / limit);
|
|
1954
|
+
return {
|
|
1955
|
+
content,
|
|
1956
|
+
page: {
|
|
1957
|
+
number: pageNumber,
|
|
1958
|
+
size: limit,
|
|
1959
|
+
offset,
|
|
1960
|
+
numberOfElements: content.length,
|
|
1961
|
+
isEmpty: content.length === 0,
|
|
1962
|
+
isFirst: pageNumber === 0,
|
|
1963
|
+
isLast: !hasNext,
|
|
1964
|
+
...sort && sort.length > 0 ? { sort: {
|
|
1965
|
+
sorted: true,
|
|
1966
|
+
fields: sort.map((s) => ({
|
|
1967
|
+
field: s.column,
|
|
1968
|
+
direction: s.direction
|
|
1969
|
+
}))
|
|
1970
|
+
} } : {}
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1901
1974
|
|
|
1902
1975
|
//#endregion
|
|
1903
1976
|
//#region ../../src/core/helpers/jsonSchemaToTypeBox.ts
|
|
@@ -2119,63 +2192,6 @@ const $env = (type) => {
|
|
|
2119
2192
|
return alepha.parseEnv(type);
|
|
2120
2193
|
};
|
|
2121
2194
|
|
|
2122
|
-
//#endregion
|
|
2123
|
-
//#region ../../src/core/primitives/$hook.ts
|
|
2124
|
-
/**
|
|
2125
|
-
* Registers a new hook.
|
|
2126
|
-
*
|
|
2127
|
-
* ```ts
|
|
2128
|
-
* import { $hook } from "alepha";
|
|
2129
|
-
*
|
|
2130
|
-
* class MyProvider {
|
|
2131
|
-
* onStart = $hook({
|
|
2132
|
-
* name: "start", // or "configure", "ready", "stop", ...
|
|
2133
|
-
* handler: async (app) => {
|
|
2134
|
-
* // await db.connect(); ...
|
|
2135
|
-
* }
|
|
2136
|
-
* });
|
|
2137
|
-
* }
|
|
2138
|
-
* ```
|
|
2139
|
-
*
|
|
2140
|
-
* Hooks are used to run async functions from all registered providers/services.
|
|
2141
|
-
*
|
|
2142
|
-
* You can't register a hook after the App has started.
|
|
2143
|
-
*
|
|
2144
|
-
* It's used under the hood by the `configure`, `start`, and `stop` methods.
|
|
2145
|
-
* Some modules also use hooks to run their own logic. (e.g. `alepha/server`).
|
|
2146
|
-
*
|
|
2147
|
-
* You can create your own hooks by using module augmentation:
|
|
2148
|
-
*
|
|
2149
|
-
* ```ts
|
|
2150
|
-
* declare module "alepha" {
|
|
2151
|
-
*
|
|
2152
|
-
* interface Hooks {
|
|
2153
|
-
* "my:custom:hook": {
|
|
2154
|
-
* arg1: string;
|
|
2155
|
-
* }
|
|
2156
|
-
* }
|
|
2157
|
-
* }
|
|
2158
|
-
*
|
|
2159
|
-
* await alepha.events.emit("my:custom:hook", { arg1: "value" });
|
|
2160
|
-
* ```
|
|
2161
|
-
*
|
|
2162
|
-
*/
|
|
2163
|
-
const $hook = (options) => createPrimitive(HookPrimitive, options);
|
|
2164
|
-
var HookPrimitive = class extends Primitive {
|
|
2165
|
-
called = 0;
|
|
2166
|
-
onInit() {
|
|
2167
|
-
this.alepha.events.on(this.options.on, {
|
|
2168
|
-
caller: this.config.service,
|
|
2169
|
-
priority: this.options.priority,
|
|
2170
|
-
callback: async (args) => {
|
|
2171
|
-
this.called += 1;
|
|
2172
|
-
await this.options.handler(args);
|
|
2173
|
-
}
|
|
2174
|
-
});
|
|
2175
|
-
}
|
|
2176
|
-
};
|
|
2177
|
-
$hook[KIND] = HookPrimitive;
|
|
2178
|
-
|
|
2179
2195
|
//#endregion
|
|
2180
2196
|
//#region ../../src/core/primitives/$use.ts
|
|
2181
2197
|
/**
|