Skip to content

Commit f1ed053

Browse files
committed
add some notes about architecture
1 parent d2b0d1c commit f1ed053

8 files changed

Lines changed: 269 additions & 105 deletions

File tree

ARCHITECTURE.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# MWS Server Architecture Explained
2+
3+
## Overview
4+
5+
The system treats a public wiki as a named, database-backed resource addressed by a slug. Internally that wiki is backed by a recipe, but for most developer-facing purposes it is simplest to think in terms of a wiki assembled from three things: a template, a set of bags, and a plugin set.
6+
7+
A template provides shared defaults such as HTML behavior, default bags, and plugin configuration. A wiki adds its own bag mappings, plugins, and access rules. At runtime, requests use the wiki's effective configuration rather than raw admin input.
8+
9+
## Core Concepts
10+
11+
A bag is a named tiddler store. Bags hold content and carry their own read and write permissions.
12+
13+
A template is a reusable definition that can be shared across many wikis. It supplies default bag structure, plugin settings, and HTML behavior.
14+
15+
A wiki is the public-facing site. It is addressed by slug and resolves to an effective set of bags and plugins.
16+
17+
Plugins extend the client runtime. The server prepares and serves the plugin assets needed to bootstrap the wiki in the browser.
18+
19+
Users, roles, and sessions determine who can see a wiki, who can edit it, and which bags they can read or write.
20+
21+
## How A Wiki Is Assembled
22+
23+
Each wiki is built from a template plus wiki-specific configuration. Together they determine which bags participate in the wiki, which of those bags are writable, which plugins are loaded, and what HTML shell the client receives.
24+
25+
Writes are routed by title prefix. In practice, the most specific matching writable prefix determines where a title is saved. Readonly bags act as fallback content layers beneath the writable part of the wiki. This gives each wiki a predictable namespace model: a title is written to one resolved writable location and read from the first applicable source in the wiki's effective bag stack.
26+
27+
Deleting follows the same model as writing. A delete applies to the resolved writable location for that title rather than removing the title from every bag that might contain it.
28+
29+
## Runtime Surface
30+
31+
The main runtime entry point is the wiki page itself:
32+
33+
- `GET /wiki/:recipe_slug` returns the HTML bootstrap for the wiki.
34+
35+
The recipe-scoped API provides the supporting runtime data:
36+
37+
- `GET /recipe/:recipe_slug/status` returns the caller's view of the wiki's bag layout and writeability.
38+
- `GET /recipe/:recipe_slug/list.json` lists visible titles together with routing information.
39+
- `GET /recipe/:recipe_slug/store.js` returns the store payload used to bootstrap the client.
40+
- `GET /recipe/:recipe_slug/updates?since=` returns recipe-scoped changes since a known revision.
41+
- `PUT /recipe/:recipe_slug/batch/:op` performs batch list, read, save, and delete operations.
42+
43+
The runtime surface is designed so that list, read, save, delete, updates, and store generation all describe the same wiki view.
44+
45+
## Store And Client Bootstrap
46+
47+
The client is bootstrapped from two pieces: HTML and store data.
48+
49+
The HTML comes either from the default TiddlyWiki shell or from template-defined custom HTML. The store contains the resolved tiddlers for the wiki plus the metadata the client needs to understand the current recipe, bag ownership, host, and revision state.
50+
51+
Depending on server and template configuration, plugin assets and store data may be delivered as external resources or injected into the HTML response. That delivery choice changes how the browser receives the data, but not what wiki content the client sees.
52+
53+
## Administration
54+
55+
The admin surface manages wikis, templates, bags, users, and roles. In this model, a wiki is the editable public resource, a template is the reusable shared definition, and a bag is the underlying storage unit.
56+
57+
Saving a wiki or template changes the configuration that future runtime requests use. Template changes affect every wiki that depends on that template. Bag changes affect where titles are stored and resolved.
58+
59+
Plugin entries are exposed through the admin load path so they can be selected and inspected, but this path does not treat plugins as editable database rows in the same way as wikis, templates, bags, users, and roles.
60+
61+
User administration covers profile-like data and role membership. Password creation, login, reset, and password change belong to the session and password subsystem rather than to admin row saves.
62+
63+
## Permissions
64+
65+
Access is role-based.
66+
67+
Recipe permissions determine whether a wiki is available to a user at all. Bag permissions determine whether the user can read from or write to the bags that make up that wiki. Template permissions govern template management rather than live wiki access.
68+
69+
The runtime presents a wiki as a coherent whole rather than a partially filtered bag set. If a user does not have the required access to the bags that define a wiki, the wiki request is denied. A user can also have access to a wiki while still being unable to modify a specific title if that title resolves to a writable bag they cannot edit.
70+
71+
## Sessions And Identity
72+
73+
Sessions resolve the current user and that user's roles for every request. Those roles are then used consistently across wiki access checks, bag write checks, and admin operations.
74+
75+
The public identifier for a wiki is its slug. Developers working on the runtime surface should treat that slug as the stable address of the wiki.
76+
77+
## Repository Layout
78+
79+
This is a monorepo divided into separate projects. The entry point is in `packages/mws/src/index.ts`. The entry point imports the `server`, `commander`, and `events` projects.
80+
81+
The entire server uses Promises and async functions for pretty much everything. Synchronous file system calls should be avoided as much as possible.
82+
83+
The `events` project is the foundation of MWS. It contains an `EventEmitter` instance that everything else subscribes to.
84+
85+
Events are emitted asyncly. Event listeners are awaited with `Promise.all`, and rejections throw back to the event emit call. This is intentional because it is the heart of the entire server, not just a public event bus, and errors shouldn't be ignored. Errors that come from things like attempting to send SSE events to other clients, however, should not be thrown because they aren't relevent to the source of the event.
86+
87+
There should be no singleton references other than the event emitter and event handlers should be as pure as possible, so that in theory it would be possible to run multiple completely separate MWS servers in the same process.
88+
89+
The `commander` project handles the CLI parsing code. It exports a default function which MWS calls to execute the CLI. Commander emits several events during execution, which MWS hooks into to scaffold the rest of the server.
90+
91+
It also exports the base class for commands to inherit from, and because it instantiates the commands, commands should not specify their own constructor. Additional instance properties may be added to commands in the `cli.execute.before` event.
92+
93+
The `server` project handles all web related stuff, but in an MWS-agnostic way. It only requires the `events` project, so it could be used as the foundation for unrelated webservers. Its internal API is directly inspired by the TiddlyWiki server API, with routing and centralized handling of the request body.
94+
95+
The `mws` project is the application layer of MWS and ties everything else together. It defines all the commands and web server routes and handles the database connection.
96+
97+
## Server Events
98+
99+
These are the hooks that run on startup. The listen command starts the server and returns, finishing the command run.
100+
101+
```
102+
├ cli.register: 0.101ms
103+
├ cli.commander: 0.021ms
104+
├ Command: listen: --------
105+
├ ├ cli.execute.before: --------
106+
├ ├ ├ mws.cache.init.before: 0.012ms
107+
├ ├ ├ mws.cache.init.after: 0.016ms
108+
├ ├ ├ mws.adapter.init.before: 0.013ms
109+
├ ├ ├ mws.adapter.init.after: 0.007ms
110+
├ ├ ├ mws.config.init.before: 0.008ms
111+
├ ├ ├ mws.config.init.after: 0.013ms
112+
├ ├ cli.execute.before: 1.175s
113+
├ ├ listen.router.init: --------
114+
├ ├ ├ mws.router.init: 0.017ms
115+
├ ├ ├ mws.routes.important: 0.004ms
116+
├ ├ ├ mws.routes: 1.054ms
117+
├ ├ ├ mws.routes.fallback: 0.039ms
118+
├ ├ listen.router.init: 111.665ms
119+
├ ├ cli.execute.after: 0.008ms
120+
├ Command: listen: 1.296s
121+
```
122+
123+
## Todo
124+
125+
- server input validation: all the pieces are there, I just need to wire it up.
126+
- make commands easier to extend, probably just by making it easier to specify a list of commands via javascript.
127+
- clean up my spaghetti code into nice straight rows of pasta.

CONTRIBUTING.md

Lines changed: 0 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,96 +2,3 @@
22

33
Contributors need to sign the TiddlyWiki CLA to, you know, contribute. \
44
To sign the CLA, and understand what it means, [go here.](https://github.com/TiddlyWiki/TiddlyWiki5/blob/master/contributing.md#contributor-license-agreement)
5-
6-
# MWS Server Architecture Explained
7-
8-
This is a monorepo divided into separate projects. The entry point is in `packages/mws/src/index.ts`. The entry point imports the `server`, `commander`, and `events` projects.
9-
10-
The entire server uses Promises and async functions for pretty much everything. Synchronous file system calls should be avoided as much as possible.
11-
12-
The `events` project is the foundation of MWS. It contains an `EventEmitter` instance that everything else subscribes to.
13-
14-
Events are emitted asyncly. Event listeners are awaited with `Promise.all`, and rejections throw back to the event emit call. This is intentional because it is the heart of the entire server, not just a public event bus, and errors shouldn't be ignored. Errors that come from things like attempting to send SSE events to other clients, however, should not be thrown because they aren't relevent to the source of the event.
15-
16-
There should be no singleton references other than the event emitter and event handlers should be as pure as possible, so that in theory it would be possible to run multiple completely separate MWS servers in the same process.
17-
18-
The `commander` project handles the CLI parsing code. It exports a default function which MWS calls to execute the CLI. Commander emits several events during execution, which MWS hooks into to scaffold the rest of the server.
19-
20-
It also exports the base class for commands to inherit from, and because it instantiates the commands, commands should not specify their own constructor. Additional instance properties may be added to commands in the `cli.execute.before` event.
21-
22-
The `server` project handles all web related stuff, but in an MWS-agnostic way. It only requires the `events` project, so it could be used as the foundation for unrelated webservers. Its internal API is directly inspired by the TiddlyWiki server API, with routing and centralized handling of the request body.
23-
24-
The `mws` project is the application layer of MWS and ties everything else together. It defines all the commands and web server routes and handles the database connection.
25-
26-
The `react-admin` package is the Admin UI. It's based on react, and is built automatically when the dev server is enabled. You can find the server route in `packages/mws/src/services/setupDevServer.ts`
27-
28-
If this all sounds a bit confusing, here's all the events that are emitted on a normal startup.
29-
30-
- `zod.make` - Emitted by `server` to extend the zod global.
31-
- `cli.register` - Emitted by `commander` to register commands.
32-
- `cli.commander` - Emitted by `commander` after routes are registered, but before help is printed.
33-
- `cli.execute.before` - Emitted by `commander` right before the command is executed. This allows adding properties to the command instance for use later in the application.
34-
35-
These next ones are attachment points for further extension of MWS. They are emitted in the main `cli.execute.before` listener that MWS attaches.
36-
37-
- `mws.cache.init.before`
38-
- `mws.cache.init.after`
39-
- `mws.adapter.init.before`
40-
- `mws.adapter.init.after`
41-
- `mws.init.before`
42-
- `mws.init.after`
43-
44-
These are emitted by the listen command, either by `mws` or by `server`.
45-
46-
- `listen.router.init`
47-
- `mws.router.init`
48-
- `mws.routes.important`
49-
- `mws.routes`
50-
- `mws.routes.fallback`
51-
52-
And finally the event emitted after the command ends.
53-
54-
- `cli.execute.after`
55-
56-
Remember that this is the order the commands start executing in. Some of them are nested. For instance, if I log the event name when it is emitted and time it, this is the output.
57-
58-
59-
```
60-
zod.make
61-
zod.make: 0.123ms
62-
63-
cli.register
64-
cli.register: 0.088ms
65-
66-
cli.commander
67-
cli.commander: 0.019ms
68-
69-
cli.execute.before
70-
mws.cache.init.before
71-
mws.cache.init.before: 0.014ms
72-
mws.cache.init.after
73-
mws.cache.init.after: 0.013ms
74-
mws.adapter.init.before
75-
mws.adapter.init.before: 0.007ms
76-
mws.adapter.init.after
77-
mws.adapter.init.after: 0.006ms
78-
mws.init.before
79-
mws.init.before: 0.008ms
80-
mws.init.after
81-
mws.init.after: 0.009ms
82-
cli.execute.before: 1.171s
83-
84-
listen.router.init
85-
mws.router.init
86-
mws.router.init: 0.012ms
87-
mws.routes.important
88-
mws.routes.important: 0.006ms
89-
mws.routes
90-
mws.routes: 2.754ms
91-
mws.routes.fallback
92-
mws.routes.fallback: 0.054ms
93-
listen.router.init: 4.002ms
94-
95-
cli.execute.after
96-
cli.execute.after: 0.015ms
97-
```

packages/commander/src/runCLI.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ export async function runCLI() {
5252
// parse the CLI options
5353
const { params, options } = parseCLI(commander, cmd, process.argv.slice(3));
5454
const inst = new cmdDef.Command(params, options);
55+
serverEvents.logBefore("Command: " + cmd);
5556
await serverEvents.emitAsync("cli.execute.before", cmd, params, options, inst);
5657
await inst.execute();
5758
await serverEvents.emitAsync("cli.execute.after", cmd, params, options, inst);
58-
59+
serverEvents.logAfter("Command: " + cmd);
5960
}
6061

6162

packages/events/src/index.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,61 @@ export interface ServerEventsMap {
1515
}
1616
// the node implementation handles once in the once handler, not in emit
1717
export class ServerEvents extends EventEmitter<ServerEventsMap> {
18+
activeCount = 0;
19+
activeName = "";
20+
// events aren't supposed to be running on the first evaluation
21+
// so this stays true until the proper code changes it.
22+
eventLogging = true;
23+
logBefore(eventName: string) {
24+
if (!this.eventLogging) return;
25+
if (this.activeName)
26+
console.log(
27+
"├ ".repeat(this.activeCount)
28+
+ String(this.activeName)
29+
+ ":", "--------"
30+
);
31+
this.activeName = String(eventName);
32+
this.activeCount++;
33+
console.time("├ ".repeat(this.activeCount) + String(eventName));
34+
}
35+
logAfter(eventName: string) {
36+
if (!this.eventLogging) return;
37+
console.timeEnd("├ ".repeat(this.activeCount) + String(eventName));
38+
this.activeCount--;
39+
this.activeName = "";
40+
}
41+
logLine(text: string) {
42+
if (!this.eventLogging) return;
43+
console.log("├ ".repeat(this.activeCount + 1) + text)
44+
}
1845
/** Use emitAsync instead */
1946
override emit!: never;
2047
/** Call all listeners in sequence sync'ly */
2148
emitSync<K>(
2249
eventName: keyof ServerEventsMap | K,
2350
...args: K extends keyof ServerEventsMap ? ServerEventsMap[K] : never
2451
) {
52+
this.logBefore(String(eventName));
2553
this.listeners(eventName).map(e => e(...args));
54+
this.logAfter(String(eventName));
2655
}
2756
/** Call all listeners via Promise.all. */
2857
async emitAsync<K>(
2958
eventName: keyof ServerEventsMap | K,
3059
...args: K extends keyof ServerEventsMap ? ServerEventsMap[K] : never
3160
) {
32-
await Promise.all(this.listeners(eventName).map(e => e(...args)));
61+
this.logBefore(String(eventName));
62+
for (const e of this.listeners(eventName)) { await e(...args) };
63+
this.logAfter(String(eventName));
3364
}
3465
/** Call all listeners sync'ly, await them, catch errors per listener and forward them to emitLogCatcher. */
3566
async emitLog<K>(
3667
eventName: keyof ServerEventsMap | K,
3768
...args: K extends keyof ServerEventsMap ? ServerEventsMap[K] : never
3869
) {
70+
this.logBefore(String(eventName));
3971
this.listeners(eventName).map(async e => { try { await e(...args) } catch (e) { this.emitLogCatcher?.(e); } })
72+
this.logAfter(String(eventName));
4073
}
4174

4275
emitLogCatcher?: (error: unknown) => void;

packages/mws/src/index.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
// import "./globals";
21
// const originalconsole = console.log.bind(console);
3-
// console.log = (...args) => { originalconsole(new Error("console stack").stack); return originalconsole(...args) };
2+
// console.log = (...args) => {
3+
// originalconsole(new Error("console stack").stack);
4+
// return originalconsole(...args)
5+
// };
46
import { install } from "source-map-support";
57
install();
68
import { serverEvents } from "@tiddlywiki/events";
79

8-
// these all use serverEvents
910
import "@tiddlywiki/commander";
1011
import "@tiddlywiki/server";
1112
import "./startup";
12-
// import "./registerStartup";
1313
import "./new-commands";
1414
import "./new-managers";
1515
import "./zodAssert";
@@ -29,12 +29,26 @@ import { clientBuildDef } from "./startup";
2929

3030
export * from "@tiddlywiki/server";
3131
export * from "@tiddlywiki/events";
32-
export { SessionManager, SessionManagerObject, AuthUser } from "./services/sessions";
33-
export { PasswordService } from "./services/PasswordService";
34-
export { WikiPluginCache, PluginDefinition, TiddlerHasher, defaultPreloadFunction } from "./services/cache";
32+
export {
33+
SessionManager,
34+
SessionManagerObject,
35+
AuthUser
36+
} from "./services/sessions";
37+
export {
38+
PasswordService
39+
} from "./services/PasswordService";
40+
export {
41+
WikiPluginCache,
42+
PluginDefinition,
43+
TiddlerHasher,
44+
defaultPreloadFunction,
45+
} from "./services/cache";
3546
export * from "./services/setupDevServer";
3647
export * from "./services/tiddlywiki";
37-
export { mountTW5Route, TW5Route } from "./services/tw-routes";
48+
export {
49+
mountTW5Route,
50+
TW5Route
51+
} from "./services/tw-routes";
3852
export * from "./new-commands";
3953
export * from "./new-managers";
4054

@@ -64,8 +78,10 @@ export default async function runMWS(oldOptions?: any) {
6478
if (process.env.CLIENT_BUILD) {
6579
await runBuildOnce(clientBuildDef);
6680
} else {
81+
serverEvents.eventLogging = !!process.env.VERBOSE;
6782
await startup();
6883
await runCLI();
84+
serverEvents.eventLogging = false;
6985
}
7086

7187
}

packages/mws/src/new-managers/NEW-DESIGN-V2.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ Permissions are role-based.
218218
### Levels
219219

220220
- Bag levels: `A_read`, `B_write`, `C_admin`
221+
- Template levels: `A_read`, `B_write`
221222
- Recipe levels: `A_read`, `B_write`
222223

223224
The prefixed enum ordering is intentional. Descending lexical order yields the highest held privilege.

0 commit comments

Comments
 (0)