Skip to content

Commit

Permalink
New BaseSystem base class. (#199)
Browse files Browse the repository at this point in the history
This PR extracts `BaseSystem` from `UsualSystem`. Though this codebase
doesn't (yet?) need multiple concrete top-level system classes, this
will be useful for downstream projects.
  • Loading branch information
danfuzz authored Dec 7, 2023
2 parents 0411af1 + 6f0da60 commit a3f928d
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 130 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Breaking changes:
* None.

Other notable changes:
* Extracted base class `BaseSystem` from `UsualSystem`, to help avoid code
duplication in downstream projects.
* Pulled in improved `bashy-lib`, and adjusted accordingly.

### v0.5.19 -- 2023-12-04
Expand Down
4 changes: 3 additions & 1 deletion scripts/lib/bashy-node/node-project/fix-package-json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ function get-deps {
local files

files=($(
lib buildy ls-files --output=lines --include='\.js$' "${srcDir}"
lib ls-files --output=lines \
--include='\.js$' --exclude='/tests/' \
"${srcDir}"
)) \
|| return "$?"

Expand Down
206 changes: 206 additions & 0 deletions src/app-util/export/BaseSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright 2022-2023 the Lactoserv Authors (Dan Bornstein et alia).
// SPDX-License-Identifier: Apache-2.0

import { Condition, Threadlet } from '@this/async';
import { Host } from '@this/host';
import { IntfLogger } from '@this/loggy';
import { Methods } from '@this/typey';


/**
* Base class to operate the top level of a system, in the usual fashion. This
* takes care of coordinating initialization, running, and shutdown, leaving
* implementation holes for a concrete subclass to take appropriate app-specific
* action.
*/
export class BaseSystem extends Threadlet {
/** @type {boolean} Initialized? */
#initDone = false;

/** @type {Condition} Was a reload requested? */
#reloadRequested = new Condition();

/**
* @type {?IntfLogger} Logger for this instance, or `null` not to do any
* logging.
*/
#logger = null;

/**
* @type {*} Value returned from {@link #_impl_init} which is currently being
* used.
*/
#initValue = null;

/**
* @type {*} Value returned from {@link #_impl_init} which is to be used on
* the next start (including a restart).
*/
#nextInitValue = null;

/**
* Constructs an instance.
*
* @param {?IntfLogger} logger The logger to use, if any.
*/
constructor(logger) {
super(
() => this.#start(),
() => this.#run());

this.#logger = logger;
}

/**
* Performs pre-start initialization.
*/
#init() {
if (this.#initDone) {
return;
}

Host.registerReloadCallback(() => this.#requestReload());
Host.registerShutdownCallback(() => this.stop());

this.#initDone = true;
}

/**
* Helper for {@link #run}, which performs a system reload.
*/
async #reload() {
this.#logger.reloading();

try {
this.#nextInitValue = await this._impl_init(true);
} catch (e) {
// Can't reload! There's was a problem during re-initialization (e.g. an
// error in the config file).
this.#logger.errorDuringReloaded(e);
this.#logger.notReloading();
return;
}

await this.#stop(true);
await this.#start(true);

this.#logger.reloaded();
}

/**
* Requests that the system be reloaded.
*/
async #requestReload() {
if (this.isRunning()) {
this.#logger.reload('requested');
this.#reloadRequested.value = true;
} else {
// Not actually running (probably in the middle of completely shutting
// down).
this.#logger.reload('ignoring');
}
}

/**
* Main thread body: Runs the system.
*/
async #run() {
if (this.#initValue === null) {
throw new Error('Shouldn\'t happen (no initialization value).');
}

while (!this.shouldStop()) {
if (this.#reloadRequested.value === true) {
await this.#reload();
this.#reloadRequested.value = false;
}

await this.raceWhenStopRequested([
this.#reloadRequested.whenTrue()
]);
}

await this.#stop();
}

/**
* System start function. Used as the thread start function and also during
* requested reloads.
*
* @param {boolean} [forReload] Is this for a reload?
*/
async #start(forReload = false) {
const logArg = forReload ? 'reload' : 'init';

this.#init();

this.#logger.starting(logArg);

if (!forReload) {
try {
this.#nextInitValue = await this._impl_init(false);
} catch (e) {
this.#logger.startAborted();
throw e;
}
}

this.#initValue = this.#nextInitValue;
this.#nextInitValue = null;
await this._impl_start(forReload, this.#initValue);

this.#logger.started(logArg);
}

/**
* System stop function. Used when the system is shutting down on the way to
* exiting, and also used during requested reloads.
*
* @param {boolean} [forReload] Is this for a reload?
*/
async #stop(forReload = false) {
const logArg = forReload ? 'willReload' : 'shutdown';

this.#logger.stopping(logArg);
await this._impl_stop(forReload, this.#initValue);
this.#logger.stopped(logArg);

this.#initValue = null;
}

/**
* Initializes any concrete-subclass-related bits, in preparation for running
* the system. If `forReload` is passed as `true`, the system is _already_
* running, and care should be taken not to disturb that. In particular, this
* method is allowed to throw, and that will cause reloading to fail while
* leaving the already-running system alone.
*
* @abstract
* @param {boolean} forReload Is this for a reload?
* @returns {*} Value to pass to {@link #_impl_start}, once it is time to
* (re-)start the system.
*/
async _impl_init(forReload) {
Methods.abstract(forReload);
}

/**
* Starts the system, in a subclass-specific way.
*
* @param {boolean} forReload Is this for a reload?
* @param {*} initValue Value previously returned from {@link #_impl_init}.
*/
async _impl_start(forReload, initValue) {
Methods.abstract(forReload, initValue);
}

/**
* Stops the system, in a subclass-specific way.
*
* @param {boolean} forReload Is this for a reload?
* @param {*} initValue Value previously returned from {@link #_impl_init}.
*/
async _impl_stop(forReload, initValue) {
Methods.abstract(forReload, initValue);
}
}
1 change: 1 addition & 0 deletions src/app-util/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2022-2023 the Lactoserv Authors (Dan Bornstein et alia).
// SPDX-License-Identifier: Apache-2.0

export * from '#x/BaseSystem';
export * from '#x/Rotator';
export * from '#x/Saver';
1 change: 1 addition & 0 deletions src/app-util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@this/app-config": "*",
"@this/async": "*",
"@this/fs-util": "*",
"@this/host": "*",
"@this/loggy": "*",
"@this/typey": "*"
}
Expand Down
1 change: 1 addition & 0 deletions src/main-lactoserv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

"dependencies": {
"@this/app-framework": "*",
"@this/app-util": "*",
"@this/async": "*",
"@this/builtin-applications": "*",
"@this/builtin-services": "*",
Expand Down
Loading

0 comments on commit a3f928d

Please sign in to comment.