@volter/world-core 3.0.161 → 3.0.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sockets.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { runWithStorageTenant, storageTenant } from './request-scope.ts';
1
2
  // A vendor's persistent wire (architecture, "Other wires: sockets"): a WebSocket its clients hold open, on which the
2
3
  // vendor sends what happens as it happens (Discord's Gateway: a bot identifies, is told the guilds it is in, and is sent
3
4
  // each message, member and interaction as it lands). The manifest declares the socket (`sockets`); the pack's engine
@@ -49,7 +50,7 @@ export type SocketEngine<C> = {
49
50
  close?(session: SocketSession, ctx: C): void | Promise<void>;
50
51
  };
51
52
 
52
- type Open = { session: SocketSession; engine: SocketEngine<unknown>; context: () => Promise<unknown>; queue: Promise<void>; pending: Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout>; due: number }> };
53
+ type Open = { tenant?: string; run: <T>(fn: () => T) => T; session: SocketSession; engine: SocketEngine<unknown>; context: () => Promise<unknown>; queue: Promise<void>; pending: Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout>; due: number }> };
53
54
  const sessions = new Map<string, Set<Open>>();
54
55
  /** A World's next session number, per World (its service and root): a session's id is the World's, never the
55
56
  * process's, so a life replayed on a fresh World numbers its sessions as it did the first time. */
@@ -58,7 +59,7 @@ const keyOf = (service: string, root: string | undefined): string => `${service}
58
59
 
59
60
  /** The open sessions of one socket of the vendor in a World (a handler reaches them as `ctx.sockets(id)`). */
60
61
  export function openSessions(service: string, root: string | undefined, socket: string): SocketSession[] {
61
- return [...(sessions.get(keyOf(service, root)) ?? [])].filter((o) => o.session.socket === socket).map((o) => o.session);
62
+ return [...(sessions.get(keyOf(service, root)) ?? [])].filter((o) => o.session.socket === socket && o.tenant === storageTenant(service)).map((o) => o.session);
62
63
  }
63
64
 
64
65
  /** What a session awaits that the World's clock has run out (a life's `wait` past a relay's timeout), rejected as the
@@ -77,8 +78,9 @@ export function socketWrite(service: string, root: string | undefined, write: Ev
77
78
  if (!open?.size) return;
78
79
  for (const o of open) {
79
80
  const { engine } = o;
81
+ if (o.tenant !== storageTenant(service)) continue;
80
82
  if (!engine.write) continue;
81
- o.queue = o.queue.then(async () => engine.write!(o.session, write, await o.context())).catch((error) => o.session.close(1011, String((error as Error).message ?? error).slice(0, 120)));
83
+ o.queue = o.queue.then(() => o.run(async () => engine.write!(o.session, write, await o.context()))).catch((error) => o.session.close(1011, String((error as Error).message ?? error).slice(0, 120)));
82
84
  }
83
85
  }
84
86
 
@@ -105,8 +107,8 @@ export function socketUpgrade<C>(
105
107
  engines: Record<string, SocketEngine<C>>,
106
108
  context: (request: Request, decl: SocketDecl, params: Record<string, string>) => Promise<C>,
107
109
  hostOf: (request: Request) => string,
108
- pathOf: (request: Request) => string = (request) => new URL(request.url).pathname,
109
- decide: (asked: unknown) => Promise<ScenarioServed | undefined> = async () => undefined,
110
+ pathOf: (request: Request) => string,
111
+ decide: (asked: unknown) => Promise<ScenarioServed | undefined>,
110
112
  ): WebSocketUpgrade {
111
113
  const matchFor = (request: Request): { decl: SocketDecl; params: Record<string, string> } | undefined => {
112
114
  const path = pathOf(request).replace(/\/+$/, '') || '/';
@@ -149,16 +151,17 @@ export function socketUpgrade<C>(
149
151
  },
150
152
  decide,
151
153
  };
152
- const o: Open = { session, engine, context: () => context(request.clone(), decl, params), queue: Promise.resolve(), pending };
154
+ const tenant = storageTenant(service), run = <T>(fn: () => T): T => runWithStorageTenant(service, tenant, fn);
155
+ const o: Open = { tenant, run, session, engine, context: () => context(request.clone(), decl, params), queue: Promise.resolve(), pending };
153
156
  byPeer.set(peer, o);
154
157
  (sessions.get(key) ?? sessions.set(key, new Set()).get(key)!).add(o);
155
- if (engine.open) o.queue = o.queue.then(async () => engine.open!(session, await o.context())).catch((error) => session.close(1011, String((error as Error).message ?? error).slice(0, 120)));
158
+ if (engine.open) o.queue = o.queue.then(() => o.run(async () => engine.open!(session, await o.context()))).catch((error) => session.close(1011, String((error as Error).message ?? error).slice(0, 120)));
156
159
  },
157
160
  message: (peer, data) => {
158
161
  const o = byPeer.get(peer);
159
162
  if (!o) return;
160
163
  const text = typeof data === 'string' ? data : new TextDecoder().decode(data);
161
- o.queue = o.queue.then(async () => o.engine.message(o.session, text, await o.context())).catch((error) => o.session.close(1011, String((error as Error).message ?? error).slice(0, 120)));
164
+ o.queue = o.queue.then(() => o.run(async () => o.engine.message(o.session, text, await o.context()))).catch((error) => o.session.close(1011, String((error as Error).message ?? error).slice(0, 120)));
162
165
  },
163
166
  close: (peer) => {
164
167
  const o = byPeer.get(peer);
@@ -168,7 +171,7 @@ export function socketUpgrade<C>(
168
171
  for (const [k, p] of o.pending) { clearTimeout(p.timer); p.reject(new Error('the session closed')); o.pending.delete(k); }
169
172
  // the end is written after what the session is still doing, over its own context
170
173
  // the session is gone, so a failure has no frame to go in: it is reported, never swallowed
171
- if (o.engine.close) o.queue = o.queue.then(async () => o.engine.close!(o.session, await o.context())).catch((error) => {
174
+ if (o.engine.close) o.queue = o.queue.then(() => o.run(async () => o.engine.close!(o.session, await o.context()))).catch((error) => {
172
175
  console.error(`${service}: the ${o.session.socket} socket's end was not written: ${String((error as Error)?.message ?? error)}`);
173
176
  });
174
177
  },
@@ -0,0 +1,4 @@
1
+ /** The retained World-account name grammar, shared by registration and read-only authority selection. */
2
+ export function isWorldAccountName(value: unknown): value is string {
3
+ return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
4
+ }