Files
Module-uo/server/model/shardPendingUnlinks/shardPendingUnlinks.db.js
wtclaude 582a55dde5
All checks were successful
PR Checks / server-tests (pull_request) Successful in 1m0s
PR Checks / client-build (pull_request) Successful in 1m0s
PR Checks / frozen-manifest (pull_request) Successful in 1m20s
feat(account): unlink game accounts on the shard before an account is deleted (MODULE_API 1.20.0)
ACCOUNT_DELETION.md §4.4 (AD10, AD11). api.onUserDeleting queues every linked
account in the new shard_pending_unlinks table, then calls the sidecar's
DELETE /link/{account} (actor web:account-deletion) and drops the mirror on 200/404.
A shard that is away leaves the row pending, retried on WS reconnect and on a live
server.hello; a protected account (403) is refused and listed on Admin -> Shard as
still linked on the shard. A live mob.login without the deleted user's webId clears
the row. coreApi ^1.20.0; core-ref pinned to website#232's merge (7bd93b8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-29 20:38:33 -05:00

51 lines
2.2 KiB
JavaScript

// Game-account unlinks that a website account's deletion could not finish
// (ACCOUNT_DELETION.md §4.4, AD11). See utils/accountDeletion.js for who writes
// and who clears these rows.
const { query } = require('../../core')
const COLS = 'account, web_user_id, state, queued_at, attempts, last_attempt_at, last_error'
// Queue an account before the first call to the shard, so a hook core abandons
// at its time limit still leaves the account here. A re-queue (the same account
// deleted twice is impossible, but a test or a retry is not) starts it over.
const queue = ({ account, webUserId }) =>
query(
`INSERT INTO shard_pending_unlinks (account, web_user_id, state)
VALUES (?, ?, 'pending')
ON DUPLICATE KEY UPDATE web_user_id = VALUES(web_user_id), state = 'pending',
attempts = 0, last_attempt_at = NULL, last_error = NULL`,
[account, webUserId],
)
const remove = (account) => query('DELETE FROM shard_pending_unlinks WHERE account = ?', [account])
// One attempt that did not finish. `state` is `pending` (try again) or `refused`
// (the shard said no for good: a protected staff account).
const recordAttempt = ({ account, state, error }) =>
query(
`UPDATE shard_pending_unlinks
SET state = ?, attempts = attempts + 1, last_attempt_at = CURRENT_TIMESTAMP, last_error = ?
WHERE account = ?`,
[state, error ? String(error).slice(0, 255) : null, account],
)
const listPending = () =>
query(`SELECT ${COLS} FROM shard_pending_unlinks WHERE state = 'pending' ORDER BY queued_at ASC`)
const listAll = () => query(`SELECT ${COLS} FROM shard_pending_unlinks ORDER BY queued_at ASC`)
// The shard just reported this account logging in with `webId` (or none). When
// that is no longer the deleted user, the tag is gone and the row has nothing
// left to wait for. Returns the number of rows dropped.
async function clearIfNotAttributed(account, webId) {
const result = await query(
`DELETE FROM shard_pending_unlinks
WHERE account = ? AND (? IS NULL OR web_user_id <> ?)`,
[account, webId, webId],
)
return Number(result && result.affectedRows) || 0
}
module.exports = { queue, remove, recordAttempt, listPending, listAll, clearIfNotAttributed }