Skip to content
57 changes: 57 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
GitHub Copilot Instructions — Impress

These instructions define the project-wide baseline for work in this repository.
Branch- or subsystem-specific architecture belongs in `.github/instructions/*.instructions.md`.
When working in an area covered by one of those files, treat the matching
instruction file as the implementation-detail source of truth.

PROJECT CONTEXT
This repository is metarhia/impress, a high-performance server runtime using:

- worker_threads concurrency
- hot-reload and filesystem watch
- strict backward-compatibility requirements across the Metarhia ecosystem

GENERAL RULES

- Do not break existing public APIs or user-visible behavior without a clear reason.
- Preserve backward compatibility unless the change explicitly requires otherwise.
- Keep code, tests, and documentation in sync.
- Prefer minimal changes that preserve the current external behavior.
- Keep module boundaries intact; do not introduce unnecessary coupling.

FILE OWNERSHIP

- `.github/copilot-instructions.md` contains only repository-wide, branch-agnostic rules.
- `AGENTS.md` contains workflow/process rules for agents.
- `.github/instructions/*.instructions.md` contains branch- or subsystem-specific implementation details.

BRANCH-AWARE INSTRUCTION SELECTION

- Instruction files in `.github/instructions/` may include a `branch` field in their YAML frontmatter.
- A file with `branch: X` applies ONLY when the current Git branch is `X`.
- When multiple instruction files match an edited file's path, determine the current Git branch and follow ONLY the instruction file whose `branch` value matches. Ignore all non-matching branch-scoped files.
- Instruction files WITHOUT a `branch` field are general and apply to all branches.
- If no instruction file matches the current branch, do not invent constraints from non-matching files.

WORKING RULES FOR INSTRUCTION FILES

- Before changing files matched by an instruction file's `applyTo`, read that instruction file.
- Multiple instruction files may coexist for different modules, subsystems, or branches; keep each focused and scoped.
- If a branch-specific instruction file and the code diverge, update the instruction file to match the code in the current branch.

SYNCING BRANCH-SPECIFIC INSTRUCTIONS

- Branch-specific instruction files are stored on the `CopilotInstructions` branch and synced to feature branches as untracked files.
- They MUST NOT be committed on feature branches or included in pull requests.
- To sync, run: `.github/scripts/sync-instructions.ps1` (PowerShell) or the equivalent shell commands.
- First-time bootstrap (when the script is not yet present locally):
`git fetch origin CopilotInstructions; git checkout origin/CopilotInstructions -- .github/instructions/ .github/scripts/; git reset HEAD -- .github/instructions/ .github/scripts/`

TESTING

- Validate changes with the existing test suite when behavior may be affected.
- Update or add tests when behavior, integration, or configuration semantics change.

FINAL RULE
This file defines only the baseline constraints for Impress. Do not place branch- or subsystem-specific architecture here; keep that in `.github/instructions/*.instructions.md`.
550 changes: 550 additions & 0 deletions VFS-SAB.md

Large diffs are not rendered by default.

49 changes: 47 additions & 2 deletions impress.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { Pool, isError } = require('metautil');
const { loadSchema } = require('metaschema');
const { Logger } = require('metalog');
const { Planner } = require('./lib/planner.js');
const { SharedCache } = require('./lib/cache/SharedCache.js');

const CONFIG_SECTIONS = ['log', 'scale', 'server', 'sessions'];
const PATH = process.cwd();
Expand Down Expand Up @@ -59,7 +60,15 @@ const broadcast = (app, data) => {
};

const startWorker = async (app, kind, port, id = ++impress.lastWorkerId) => {
const workerData = { id, kind, root: app.root, path: app.path, port };
const sharedCache = app.sharedCache.snapshot();
const workerData = {
id,
kind,
root: app.root,
path: app.path,
port,
sharedCache,
};
const execArgv = [...process.execArgv, `--test-reporter=${REPORTER_PATH}`];
const options = { trackUnmanagedFds: true, workerData, execArgv };
const worker = new Worker(WORKER_PATH, options);
Expand All @@ -74,6 +83,7 @@ const startWorker = async (app, kind, port, id = ++impress.lastWorkerId) => {
});

worker.on('exit', (code) => {
app.sharedCache.handleWorkerExit(id);
if (code !== 0) startWorker(app, kind, port, id);
else app.threads.delete(id);
if (impress.initialization) exit('Can not start Application server', 1);
Expand Down Expand Up @@ -128,6 +138,10 @@ const startWorker = async (app, kind, port, id = ++impress.lastWorkerId) => {
terminate: ({ code }) => {
process.emit('TERMINATE', code);
},

'ack-update': ({ updateId }) => {
app.sharedCache.handleAck(updateId, id);
},
};

worker.on('message', (msg) => {
Expand Down Expand Up @@ -175,9 +189,40 @@ const loadApplication = async (root, dir, master) => {
impress.config = config;
}
const { balancer, ports = [], workers = {} } = config.server;
const { cache = {} } = config;
const threads = new Map();
const cacheOptions = {
limit: cache.sab?.limit,
baseSegmentSize: cache.sab?.baseSegmentSize,
maxFileSize: cache.maxFileSize,
watchTimeout: config.server.timeouts.watch,
placements: cache.placements,
dir,
console: impress.console,
broadcast: (data) => {
for (const thread of threads.values()) thread.postMessage(data);
},
getWorkerIds: () => threads.keys(),
};
const sharedCache = new SharedCache(cacheOptions);
try {
await sharedCache.initialize();
} catch (error) {
error.message = `Shared cache init failed: ${error.message}`;
throw error;
}

const pool = new Pool({ timeout: workers.wait });
const app = { root, path: dir, config, threads, pool, ready: 0 };
const app = {
root,
path: dir,
config,
threads,
pool,
ready: 0,
sharedCache,
};
sharedCache.watch();
if (balancer) await startWorker(app, 'balancer', balancer);
for (const port of ports) await startWorker(app, 'server', port);
const poolSize = workers.pool || 0;
Expand Down
33 changes: 30 additions & 3 deletions lib/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Application extends EventEmitter {
this.watcher = null;
this.semaphore = null;
this.server = null;
this.sharedCache = null;
}

absolute(relative) {
Expand All @@ -70,14 +71,16 @@ class Application extends EventEmitter {
}
}

async load({ invoke }) {
async load({ invoke, sharedCache }) {
if (sharedCache) {
this.sharedCache = sharedCache;
this.applySharedCache(sharedCache);
}
this.startWatch();
this.createSandbox();
this.sandbox.application.invoke = invoke;
this.sandbox.application.emit('loading');
await this.parallel([
this.static.load(),
this.resources.load(),
this.cert.load(),
(async () => {
await this.schemas.load();
Expand All @@ -94,6 +97,25 @@ class Application extends EventEmitter {
await this.start();
}

applySharedCache(sharedCache) {
const { projectEntry, config } = this;
const { filesystems } = sharedCache;
for (const name of Object.keys(filesystems)) {
const index = filesystems[name];
const entries =
index.entries instanceof Map ? index.entries : new Map(index.entries);
const files = new Map();
for (const [key, entry] of entries) {
files.set(key, projectEntry(entry));
}
const place = this[name];
if (!place) continue;
place.setFiles(files);
if (place.initServing) place.initServing(config);
}
sharedCache.segments = null;
}

async start() {
const { sandbox, config, cert, mode } = this;
const { kind, port } = workerData;
Expand Down Expand Up @@ -198,11 +220,15 @@ class Application extends EventEmitter {
startWatch() {
const timeout = this.config.server.timeouts.watch;
this.watcher = new DirectoryWatcher({ timeout });
const shared = this.sharedCache
? new Set(Object.keys(this.sharedCache.filesystems))
: new Set();

this.watcher.on('change', (filePath) => {
const relPath = filePath.substring(this.path.length + 1);
const sepIndex = relPath.indexOf(node.path.sep);
const place = relPath.substring(0, sepIndex);
if (shared.has(place)) return;
node.fs.stat(filePath, (error, stat) => {
if (error) return;
if (stat.isDirectory()) return void this[place].load(filePath);
Expand All @@ -215,6 +241,7 @@ class Application extends EventEmitter {
const relPath = filePath.substring(this.path.length + 1);
const sepIndex = relPath.indexOf(node.path.sep);
const place = relPath.substring(0, sepIndex);
if (shared.has(place)) return;
this[place].delete(filePath);
if (threadId === 1) this.console.debug('Deleted: /' + relPath);
});
Expand Down
Loading