alepha 0.13.8 → 0.14.0

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