Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bootLibrariesFirst #53

Merged
merged 9 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,3 @@ Each is designed to extend the capabilities the core library, offering specializ
| `hass` | Home Assistant integration for smart home automation. | [GitHub](https://github.com/Digital-Alchemy-TS/hass) | [npm](https://www.npmjs.com/package/@digital-alchemy/hass) |
| `synapse` | Tools for generating entities within home assistant | [GitHub](https://github.com/Digital-Alchemy-TS/synapse) | [npm](https://www.npmjs.com/package/@digital-alchemy/synapse) |
| `automation` | Advanced automation tools for creating dynamic workflows. | [GitHub](https://github.com/Digital-Alchemy-TS/automation) | [npm](https://www.npmjs.com/package/@digital-alchemy/automation) |
| `terminal` | Tools and components for complex terminal applications | [GitHub](https://github.com/Digital-Alchemy-TS/terminal) | [npm](https://www.npmjs.com/package/@digital-alchemy/terminal) |
| `gotify` | Integration for Gotify notifications, enabling seamless alerting and messaging. | [GitHub](https://github.com/Digital-Alchemy-TS/gotify) | [npm](https://www.npmjs.com/package/@digital-alchemy/gotify-extension) |
25 changes: 24 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "@digital-alchemy/core",
"description": "Application wiring, configuration, and boilerplate utilities",
"repository": {
"url": "git+https://github.com/Digital-Alchemy-TS/core"
},
"version": "24.7.2",
"version": "24.8.1",
"author": {
"url": "https://github.com/zoe-codez",
"name": "Zoe Codez"
Expand All @@ -16,6 +17,28 @@
"test": "./scripts/test.sh",
"upgrade": "ncu -u; yarn"
},
"bugs": {
"email": "[email protected]",
"url": "https://github.com/Digital-Alchemy-TS/core/issues/new/choose"
},
"keywords": [
"nodejs",
"boilerplate",
"automation",
"typescript",
"core",
"digital-alchemy"
],
"funding": [
{
"url": "https://github.com/sponsors/zoe-codez",
"type": "GitHub"
},
{
"url": "https://ko-fi.com/zoe_codez",
"type": "ko-fi"
}
],
"exports": {
".": "./dist/index.js"
},
Expand Down
4 changes: 4 additions & 0 deletions src/extensions/is.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type MaybeEmptyTypes =
| string
| undefined
| Array<unknown>
| number
| Set<unknown>
| Map<unknown, unknown>
| object;
Expand Down Expand Up @@ -53,6 +54,9 @@ export class IsIt {
}
return true;
}
if (typeof test === "number") {
return Number.isNaN(test);
}
// Optional: Throw an error or return a default value for unsupported types
throw new Error("Unsupported type " + typeof test);
}
Expand Down
46 changes: 36 additions & 10 deletions src/extensions/wiring.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function CreateBoilerplate() {
description: "Redis is preferred if available",
enum: ["redis", "memory"],
type: "string",
} as StringConfig<`${CacheProviders}`>,
} satisfies StringConfig<`${CacheProviders}`>,
CACHE_TTL: {
default: 86_400,
description: "Configuration property for cache provider, in seconds",
Expand All @@ -78,7 +78,7 @@ function CreateBoilerplate() {
description: "Minimum log level to process",
enum: ["silent", "trace", "info", "warn", "debug", "error"],
type: "string",
} as StringConfig<TConfigLogLevel>,
} satisfies StringConfig<TConfigLogLevel>,
REDIS_URL: {
default: "redis://localhost:6379",
description:
Expand Down Expand Up @@ -285,20 +285,27 @@ async function WireService(
}

const runPreInit = async (internal: InternalDefinition) => {
await internal.boot.lifecycle.exec("PreInit");
const duration = await internal.boot.lifecycle.exec("PreInit");
internal.boot.completedLifecycleEvents.add("PreInit");
return duration;
};

const runPostConfig = async (internal: InternalDefinition) => {
await internal.boot.lifecycle.exec("PostConfig");
const duration = await internal.boot.lifecycle.exec("PostConfig");
internal.boot.completedLifecycleEvents.add("PostConfig");
return duration;
};

const runBootstrap = async (internal: InternalDefinition) => {
await internal.boot.lifecycle.exec("Bootstrap");
const duration = await internal.boot.lifecycle.exec("Bootstrap");
internal.boot.completedLifecycleEvents.add("Bootstrap");
return duration;
};

const runReady = async (internal: InternalDefinition) => {
await internal.boot.lifecycle.exec("Ready");
const duration = await internal.boot.lifecycle.exec("Ready");
internal.boot.completedLifecycleEvents.add("Ready");
return duration;
};

// #MARK: Bootstrap
Expand Down Expand Up @@ -394,11 +401,15 @@ async function Bootstrap<
CONSTRUCT[i.name] = `${Date.now() - start}ms`;
});

logger.info({ name: Bootstrap }, `init application`);
// * Finally the application
start = Date.now();
await application[WIRE_PROJECT](internal, WireService);
CONSTRUCT[application.name] = `${Date.now() - start}ms`;
if (options.bootLibrariesFirst) {
logger.warn({ name: Bootstrap }, `bootLibrariesFirst`);
} else {
logger.info({ name: Bootstrap }, `init application`);
start = Date.now();
await application[WIRE_PROJECT](internal, WireService);
CONSTRUCT[application.name] = `${Date.now() - start}ms`;
}

// ? Configuration values provided bootstrap take priority over module level
if (!is.empty(options?.configuration)) {
Expand All @@ -424,6 +435,21 @@ async function Bootstrap<
`[Bootstrap] running lifecycle callbacks`,
);
STATS.Bootstrap = await runBootstrap(internal);

if (options.bootLibrariesFirst) {
// * mental note
// running between bootstrap & ready seems most appropriate
// resources are expected to *technically* be ready at this point, but not finalized
// reference examples:
// - hass: socket is open & resources are ready
// - fastify: bindings are available but port isn't listening

logger.info({ name: Bootstrap }, `late init application`);
start = Date.now();
await application[WIRE_PROJECT](internal, WireService);
CONSTRUCT[application.name] = `${Date.now() - start}ms`;
}

logger.debug({ name: Bootstrap }, `[Ready] running lifecycle callbacks`);
STATS.Ready = await runReady(internal);

Expand Down
4 changes: 4 additions & 0 deletions src/helpers/utilities.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export function sleep(target: number | Date = SECOND): SleepReturn {
}

export const ACTIVE_THROTTLE = new Map<string, SleepReturn>();

/**
* > 🦶🔫 - careful about creating memory leaks!
*/
export async function throttle(
identifier: string,
timeout: number,
Expand Down
10 changes: 10 additions & 0 deletions src/helpers/wiring.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,16 @@ export type BootstrapOptions = {
*/
appendService?: ServiceMap;

/**
* Finish the bootstrap sequence for the libraries before loading the application services.
*
* - **pro**: easier to write code / you are not affected by lifecycle events
* - **con**: unable to meaningfully interact with bootstrap lifecycle events if you want to
*
* You can change later, but your code may require modifications
*/
bootLibrariesFirst?: boolean;

/**
* default: true
*/
Expand Down
152 changes: 152 additions & 0 deletions src/testing/is.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { is } from "../extensions";

describe("IsIt class", () => {
test("is.array returns true for arrays", () => {
expect(is.array([])).toBe(true);
expect(is.array([1, 2, 3])).toBe(true);
expect(is.array("not an array")).toBe(false);
});

test("is.boolean returns true for booleans", () => {
expect(is.boolean(true)).toBe(true);
expect(is.boolean(false)).toBe(true);
expect(is.boolean(0)).toBe(false);
});

test("is.context returns true for strings", () => {
expect(is.context("a context string")).toBe(true);
expect(is.context(123)).toBe(false);
});

test("is.date returns true for Date objects", () => {
expect(is.date(new Date())).toBe(true);
expect(is.date("not a date")).toBe(false);
});

describe("is.empty", () => {
test("returns true for undefined", () => {
expect(is.empty(undefined)).toBe(true);
});

test("returns true for an empty string", () => {
expect(is.empty("")).toBe(true);
});

test("returns false for a non-empty string", () => {
expect(is.empty("not empty")).toBe(false);
});

test("returns true for an empty array", () => {
expect(is.empty([])).toBe(true);
});

test("returns false for a non-empty array", () => {
expect(is.empty([1, 2, 3])).toBe(false);
});

test("returns true for an empty Set", () => {
expect(is.empty(new Set())).toBe(true);
});

test("returns false for a non-empty Set", () => {
expect(is.empty(new Set([1]))).toBe(false);
});

test("returns true for an empty Map", () => {
expect(is.empty(new Map())).toBe(true);
});

test("returns false for a non-empty Map", () => {
expect(is.empty(new Map([["key", "value"]]))).toBe(false);
});

test("returns true for an empty object", () => {
expect(is.empty({})).toBe(true);
});

test("returns false for a non-empty object", () => {
expect(is.empty({ key: "value" })).toBe(false);
});

test("returns false for numbers", () => {
expect(is.empty(0)).toBe(false);
expect(is.empty(1)).toBe(false);
expect(is.empty(-1)).toBe(false);
});

test("returns false for NaN", () => {
expect(is.empty(Number.NaN)).toBe(true);
});

test("throws an error for unsupported types like boolean", () => {
// @ts-expect-error that's the test
expect(() => is.empty(true)).toThrow("Unsupported type boolean");
});

test("throws an error for unsupported types like function", () => {
expect(() => is.empty(() => {})).toThrow("Unsupported type function");
});

test("throws an error for unsupported types like symbol", () => {
// @ts-expect-error that's the test
expect(() => is.empty(Symbol.for("test"))).toThrow(
"Unsupported type symbol",
);
});
});

test("is.equal returns true for deeply equal objects", () => {
expect(is.equal({ a: 1 }, { a: 1 })).toBe(true);
expect(is.equal({ a: 1 }, { a: 2 })).toBe(false);
expect(is.equal([1, 2, 3], [1, 2, 3])).toBe(true);
expect(is.equal([1, 2, 3], [3, 2, 1])).toBe(false);
});

test("is.even returns true for even numbers", () => {
expect(is.even(2)).toBe(true);
expect(is.even(3)).toBe(false);
});

test("is.function returns true for functions", () => {
expect(is.function(() => {})).toBe(true);
expect(is.function(function () {})).toBe(true);
expect(is.function("not a function")).toBe(false);
});

test("is.number returns true for numbers", () => {
expect(is.number(123)).toBe(true);
expect(is.number(Number.NaN)).toBe(false);
expect(is.number("not a number")).toBe(false);
});

test("is.object returns true for objects", () => {
expect(is.object({})).toBe(true);
expect(is.object([])).toBe(false);
expect(is.object(null)).toBe(false);
});

test("is.random returns an element from the list", () => {
const list = [1, 2, 3, 4, 5];
expect(list).toContain(is.random(list));
});

test("is.string returns true for strings", () => {
expect(is.string("a string")).toBe(true);
expect(is.string(123)).toBe(false);
});

test("is.symbol returns true for symbols", () => {
expect(is.symbol(Symbol())).toBe(true);
expect(is.symbol("not a symbol")).toBe(false);
});

test("is.undefined returns true for undefined", () => {
expect(is.undefined(undefined)).toBe(true);
expect(is.undefined(null)).toBe(false);
});

test("is.unique returns an array of unique elements", () => {
expect(is.unique([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);
expect(is.unique(["a", "b", "b", "c"])).toEqual(["a", "b", "c"]);
});
});
26 changes: 26 additions & 0 deletions src/testing/wiring.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,32 @@ describe("Wiring", () => {

// #region Bootstrap
describe("Bootstrap", () => {
it("constructs app in between boot and ready for bootLibrariesFirst", async () => {
application = CreateApplication({
configurationLoaders: [],
// @ts-expect-error Testing
name: "testing",
services: {
Test({ internal }: TServiceParams) {
expect(
internal.boot.completedLifecycleEvents.has("Bootstrap"),
).toBe(true);
expect(internal.boot.completedLifecycleEvents.has("PreInit")).toBe(
true,
);
expect(
internal.boot.completedLifecycleEvents.has("PostConfig"),
).toBe(true);
expect(internal.boot.completedLifecycleEvents.has("Ready")).toBe(
false,
);
},
},
});
//
await application.bootstrap({ ...BASIC_BOOT, bootLibrariesFirst: true });
});

it("should prioritize services with priorityInit", async () => {
const list = [] as string[];
application = CreateApplication({
Expand Down
Loading