Compare commits
1 Commits
edge
...
feature/mo
| Author | SHA1 | Date | |
|---|---|---|---|
| 92c6972344 |
@@ -11,10 +11,6 @@
|
||||
// that the two files can drift, so a test asserts they agree
|
||||
// (client/test/moduleRegistry.test.js) rather than trusting a bump to remember
|
||||
// both.
|
||||
// 1.4.0 — a rule, not a member: §2.7 forbids a module opening a connection to a
|
||||
// game server from the website process (it talks to a sidecar, which owns the
|
||||
// durable copy). Nothing on window.__rg changed and nothing on the server's ctx
|
||||
// changed either; this half bumps because the two halves state ONE version.
|
||||
// 1.3.0 — three additions, all from Phase 3 slice 3 needing them: a nav item may
|
||||
// carry an `icon` component (§3.3), core declares a third slot
|
||||
// `player.invite.accepted` (§3.7), and `window.__rg.api` gained `BASE`, which
|
||||
@@ -30,4 +26,4 @@
|
||||
// but the two halves state ONE version: a module declares a single coreApi range
|
||||
// and is served one chunk, so a client that claimed 1.0.0 while the server
|
||||
// answered 1.1.0 would be two answers to one question.
|
||||
export const MODULE_API_VERSION = '1.4.0'
|
||||
export const MODULE_API_VERSION = '1.3.0'
|
||||
|
||||
@@ -9,15 +9,6 @@
|
||||
// Deliberately separate from PROTOCOL_VERSION (which versions the shard wire and
|
||||
// has nothing to say about a website module) and from any module's own version.
|
||||
|
||||
// 1.4.0 — no member changed. §2.7 gained one prohibition: a module does not open
|
||||
// a connection to a game server from the website process; it talks to a sidecar,
|
||||
// which owns the durable copy of the game's state. Minor rather than major
|
||||
// because the SURFACE is identical to 1.3.0 — module-uo's `coreApi: "^1.3.0"`
|
||||
// still resolves, and it already complies — but a module written against 1.3.0
|
||||
// could satisfy every member and still be built the wrong way round, which is
|
||||
// what this number now says. The one §2.7 rule with no CI behind it: an outbound
|
||||
// socket is not statically detectable the way an internal require is.
|
||||
//
|
||||
// 1.3.0 — three CLIENT additions from Phase 3 slice 3: a nav item may carry an
|
||||
// `icon`, core declares a `player.invite.accepted` slot, and `window.__rg.api`
|
||||
// gained `BASE` (which §3.5 always specified and shared.js never published).
|
||||
@@ -35,6 +26,6 @@
|
||||
// an admin action a module performs belongs in core's one audit log, the
|
||||
// extension slot needs the user its prefix names, and §2.7 forbids a module
|
||||
// reading core's `APP_BASE_URL` for itself. Additions only, so minor.
|
||||
const MODULE_API_VERSION = '1.4.0'
|
||||
const MODULE_API_VERSION = '1.3.0'
|
||||
|
||||
module.exports = { MODULE_API_VERSION }
|
||||
|
||||
@@ -265,13 +265,9 @@ async function disable(req, res) {
|
||||
// this is about to delete, so after an uninstall there is nothing left to purge
|
||||
// with. Ticking the box is the last moment the file exists.
|
||||
//
|
||||
// The order below is the whole of it: locate the purge file, STOP, purge, then
|
||||
// delete. Only the last step is destructive to the filesystem, so the file stays
|
||||
// readable the whole way through — which is why stopping comes first. Dropping a
|
||||
// module's tables while it is still started leaves it serving and ingesting
|
||||
// against a schema that no longer exists: module-uo's uo-link WebSocket keeps
|
||||
// writing shard events for as long as its onShutdown takes to run, and requests
|
||||
// in flight answer 500 where a stopped module answers 404.
|
||||
// The order below is the whole of it, and each step depends on the one above:
|
||||
// purge while the SQL is still readable, stop while the code is still loaded,
|
||||
// then delete.
|
||||
async function remove(req, res) {
|
||||
const { id } = req.params
|
||||
const purge = req.query.purge === 'true' || req.query.purge === '1'
|
||||
@@ -280,26 +276,23 @@ async function remove(req, res) {
|
||||
const onVolume = install.isInstalled(id)
|
||||
if (!current && !onVolume) return res.status(404).json({ message: 'No such module.' })
|
||||
|
||||
// Resolved before anything happens, because this branch is a 400: a request
|
||||
// that is going to be refused must not stop the module on its way out.
|
||||
let purgeSql = null
|
||||
let purged = null
|
||||
if (purge) {
|
||||
purgeSql = install.purgeFile(id)
|
||||
if (!purgeSql) {
|
||||
const file = install.purgeFile(id)
|
||||
if (!file) {
|
||||
return res.status(400).json({
|
||||
message: 'This module ships no purge.sql, so its data cannot be deleted. Uninstall without purging instead.',
|
||||
})
|
||||
}
|
||||
purged = await schema.runPurge(file)
|
||||
}
|
||||
|
||||
// Stop it before its tables and then its files vanish. A module whose
|
||||
// directory is deleted out from under a running onShutdown is being asked to
|
||||
// tear down a world whose code may already be half-unreadable — and its
|
||||
// sockets would otherwise stay open until the restart, holding a connection
|
||||
// on behalf of a module that no longer exists on disk.
|
||||
// Stop it before its files vanish. A module whose directory is deleted out
|
||||
// from under a running onShutdown is being asked to tear down a world whose
|
||||
// code may already be half-unreadable — and its sockets would otherwise stay
|
||||
// open until the restart, holding a connection on behalf of a module that no
|
||||
// longer exists on disk.
|
||||
await lifecycle.stop(id)
|
||||
|
||||
const purged = purgeSql ? await schema.runPurge(purgeSql) : null
|
||||
const removed = await install.removeDir(id)
|
||||
|
||||
// A purge leaves nothing: no directory, no tables, no data. Keeping a
|
||||
|
||||
@@ -11,11 +11,9 @@
|
||||
// - **enable must not touch the loader.** Disable ran the module's onShutdown;
|
||||
// there is no onBoot re-dispatch, so flipping the record back would put a
|
||||
// module with closed sockets and cleared timers back on the nav.
|
||||
// - **uninstall's four steps have exactly one valid order**: stop, purge,
|
||||
// remove the directory, remove the row. purge.sql lives inside that
|
||||
// directory, so purging after the delete silently does nothing with a 200 —
|
||||
// and purging before the stop drops the tables under a module that is still
|
||||
// running. See the test itself for why neither is visible in a response.
|
||||
// - **purge must run before the directory is removed.** purge.sql lives inside
|
||||
// that directory. Reorder those two lines and the feature silently stops
|
||||
// working, with a 200 and no data deleted.
|
||||
//
|
||||
// Point the DB at a closed port BEFORE requiring anything that builds the pool.
|
||||
process.env.DB_HOST = '127.0.0.1'
|
||||
@@ -335,20 +333,10 @@ test('a shutdown hook that failed is reported rather than swallowed', async () =
|
||||
|
||||
// ── uninstall and purge ────────────────────────────────────────────────────
|
||||
|
||||
test('uninstall stops the module, THEN purges, THEN removes the directory', async () => {
|
||||
// Two orderings pinned by one assertion, because both are one line away from
|
||||
// being wrong and neither failure is visible in the response:
|
||||
//
|
||||
// - purge BEFORE removeDir, or decision 5 stops working entirely — purge.sql
|
||||
// is a file inside the directory being deleted, and the endpoint would
|
||||
// still answer 200 having deleted nothing.
|
||||
// - stop BEFORE purge, or the tables are dropped under a module that is
|
||||
// still started. It keeps serving and ingesting against a schema that no
|
||||
// longer exists for as long as its onShutdown takes (module-uo's uo-link
|
||||
// WebSocket keeps writing shard events), and requests in flight answer 500
|
||||
// where a stopped module answers 404. Nothing about the old order was
|
||||
// required: purge.sql stays readable until removeDir, which is the only
|
||||
// step that touches the filesystem.
|
||||
test('uninstall purges BEFORE it removes the directory', async () => {
|
||||
// The ordering that makes decision 5 work at all: purge.sql is a file inside
|
||||
// the directory being deleted. Swap these two and the endpoint still answers
|
||||
// 200 and deletes nothing.
|
||||
const order = []
|
||||
modules.get = async (id) => ({ id, state: 'started' })
|
||||
install.isInstalled = () => true
|
||||
@@ -361,31 +349,12 @@ test('uninstall stops the module, THEN purges, THEN removes the directory', asyn
|
||||
const res = mockRes()
|
||||
await ctrl.remove(req({ params: { id: 'uo' }, query: { purge: 'true' } }), res)
|
||||
|
||||
assert.deepEqual(order, ['stop', 'purge', 'removeDir', 'removeRow'])
|
||||
assert.deepEqual(order, ['purge', 'stop', 'removeDir', 'removeRow'])
|
||||
assert.equal(res.body.purged, 12)
|
||||
assert.equal(res.body.restartRequired, true)
|
||||
assert.equal(logged[0].action, 'module.purge')
|
||||
})
|
||||
|
||||
test('a refused purge does not stop the module on its way out', async () => {
|
||||
// The 400 branch below is resolved BEFORE anything is stopped. A request that
|
||||
// is going to be refused must leave the module exactly as it found it —
|
||||
// otherwise "you cannot delete this module's data" would silently take the
|
||||
// module down as a side effect of saying no.
|
||||
const order = []
|
||||
modules.get = async (id) => ({ id, state: 'started' })
|
||||
install.isInstalled = () => true
|
||||
install.purgeFile = () => null
|
||||
lifecycle.stop = async () => { order.push('stop'); return { stopped: true, error: null } }
|
||||
install.removeDir = async () => { order.push('removeDir'); return true }
|
||||
|
||||
const res = mockRes()
|
||||
await ctrl.remove(req({ params: { id: 'uo' }, query: { purge: 'true' } }), res)
|
||||
|
||||
assert.equal(res.statusCode, 400)
|
||||
assert.deepEqual(order, [])
|
||||
})
|
||||
|
||||
test('a plain uninstall keeps the row and does not purge', async () => {
|
||||
const order = []
|
||||
modules.get = async (id) => ({ id, state: 'started' })
|
||||
|
||||
Reference in New Issue
Block a user