@homie-rocks/studio 0.47.0 → 0.48.1

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/worker/hosted.mjs CHANGED
@@ -19,6 +19,7 @@ import { compileMap, compileRules } from '../rules/rules.ts';
19
19
  /** game id → { rules, tune, map, settings, seats, build } as the build wrote it, and what compiling each gave. */
20
20
  const games = new Map();
21
21
  const compiled = new Map();
22
+ const readiness = new WeakMap();
22
23
 
23
24
  /** The table of this studio's server-hosted games (the build's site/src/rules/index.mjs). Called once, at load. */
24
25
  export function hostRules(table) {
@@ -33,6 +34,21 @@ export function hostRules(table) {
33
34
  export const hostedGame = (id) => games.has(id);
34
35
  export const hostedBuild = (id) => games.get(id);
35
36
 
37
+ /** Optional engines load at room start. Ordinary games keep their synchronous startup path. */
38
+ export function prepareHost(id) {
39
+ const entry = games.get(id);
40
+ if (!entry?.initialize) return;
41
+ const previous = readiness.get(entry);
42
+ if (previous === true) return;
43
+ if (previous) return previous;
44
+ const pending = Promise.resolve().then(() => entry.initialize()).then(
45
+ () => { readiness.set(entry, true); },
46
+ error => { readiness.delete(entry); throw error; },
47
+ );
48
+ readiness.set(entry, pending);
49
+ return pending;
50
+ }
51
+
36
52
  /** A game's rules, compiled once per isolate. Throws, naming the declaration, when they do not fit the contract. */
37
53
  export function compiledFor(id) {
38
54
  if (compiled.has(id)) return compiled.get(id);
package/worker/index.mjs CHANGED
@@ -89,7 +89,7 @@ import { appRole, openPath } from './app-format.mjs';
89
89
  */
90
90
  import { playerEnabled, EMBED_GAME_JS } from './embed.mjs';
91
91
  import { NetRoom, WATCH_POLICIES, departure, errorLine, versionOf } from './room.mjs';
92
- import { hostedGame, hostedBuild, compiledFor, startHost } from './hosted.mjs';
92
+ import { hostedGame, hostedBuild, compiledFor, prepareHost, startHost } from './hosted.mjs';
93
93
  import { roomStore, restoreDelay, LOOSE_SAVE } from './room-store.mjs';
94
94
  import { appCors, isAppOrigin } from './standalone.mjs';
95
95
  import { ROOM_ID, badRoomPage, embedAncestors, embedPreview, frameAncestors, noWatchPage, playPage, shellKey, shellStandIn, watchPage } from './pages.mjs';
@@ -1592,13 +1592,13 @@ export class Table {
1592
1592
  this.say({ ev: 'persist-failed', build: hostedBuild(this.game)?.build, error: this.storageHealth.message });
1593
1593
  }
1594
1594
 
1595
- readRoom(validateHost = false) {
1595
+ readRoom(validateHost = false, alreadyRead) {
1596
1596
  if (this.skipSave) { this.skipSave = false; return null; }
1597
1597
  try {
1598
1598
  const boot = this.durable?.boots();
1599
1599
  this.persistentStored = boot?.persistent === true;
1600
1600
  if (boot?.retired === true) { this.clearRoom(); return null; }
1601
- const saved = this.durable?.read();
1601
+ const saved = alreadyRead ?? this.durable?.read();
1602
1602
  if (!saved) return null;
1603
1603
  if (saved.v !== 1 || typeof saved.game !== 'string' || typeof saved.room !== 'string' ||
1604
1604
  this.game && saved.game !== this.game || this.code && saved.room !== this.code ||
@@ -1759,7 +1759,18 @@ export class Table {
1759
1759
  if (!room || !this.hostRt) {
1760
1760
  if (this.ctx.storage.sql?.exec("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'save'").toArray().length) {
1761
1761
  this.durable ??= this.openStore(false);
1762
- const saved = this.cleanupPending ? null : this.readRoom(true);
1762
+ let saved = this.cleanupPending ? null : this.readRoom();
1763
+ if (saved) {
1764
+ const generation = this.cleanupGeneration;
1765
+ const preparing = prepareHost(saved.game);
1766
+ if (preparing) {
1767
+ await preparing;
1768
+ // A join or office action can arrive while the optional engine loads.
1769
+ // Re-read that lifecycle instead of writing the pre-await checkpoint.
1770
+ if (this.room !== room || this.hostRt || this.cleanupGeneration !== generation) return this.runRoomAlarm();
1771
+ }
1772
+ saved = this.readRoom(true, saved);
1773
+ }
1763
1774
  if (saved) {
1764
1775
  this.game = saved.game; this.code = saved.room;
1765
1776
  if (this.isPersistent()) return;
@@ -2167,6 +2178,8 @@ export class Table {
2167
2178
  if (url.pathname === '/__recall') {
2168
2179
  const { account } = await request.json();
2169
2180
  return this.withCharacter(account, async () => {
2181
+ const preparing = prepareHost(game);
2182
+ if (preparing) await preparing;
2170
2183
  this.ensureHost(room, true);
2171
2184
  if (!this.hostRt || this.persistentFailed || room.hostFailed) return json({ ok: false }, 503);
2172
2185
  await this.flushCharacters();
@@ -2232,6 +2245,8 @@ export class Table {
2232
2245
  const said = url.searchParams.has('cur');
2233
2246
  if (said) { room.setStall(url.searchParams.get('stall')); room.setCurrent(url.searchParams.get('cur') || null); }
2234
2247
  // Restore before applying the Worker's current policy: a save must not replace a newer policy.
2248
+ const preparing = prepareHost(game);
2249
+ if (preparing) await preparing;
2235
2250
  this.ensureHost(room, url.searchParams.get('host') === 'server');
2236
2251
  // The room's policy (section 17), composed by the Worker for every socket: a newer one than the room's applies.
2237
2252
  const pol = decodeFacts(url.searchParams.get('pol'));
@@ -1,2 +1,2 @@
1
1
  /** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
2
- export const STUDIO_VERSION_TAG = '0.47.0';
2
+ export const STUDIO_VERSION_TAG = '0.48.1';