@homie-rocks/studio 0.58.8 → 0.58.11
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/CHANGELOG.md +35 -0
- package/README.md +34 -0
- package/bin/homie-studio.mjs +19 -2
- package/guides/art/scripts/art.mjs +4 -3
- package/guides/game/RULES.md +2 -0
- package/guides/game/SKILL.md +2 -0
- package/guides/playtest/scripts/playtest.mjs +4 -4
- package/guides/video/scripts/lib/browser.mjs +23 -2
- package/lib/check.mjs +4 -4
- package/lib/chrome.mjs +23 -0
- package/lib/cloudflare.mjs +18 -8
- package/lib/crowd-load.mjs +2 -1
- package/lib/dispatch-target.mjs +49 -0
- package/lib/lab-check.mjs +3 -3
- package/lib/look.mjs +3 -3
- package/lib/mcp-tools.mjs +10 -0
- package/lib/move-drain.mjs +18 -0
- package/lib/perf.mjs +3 -3
- package/lib/port-check.mjs +3 -3
- package/lib/read-only.mjs +69 -0
- package/lib/render3d.mjs +3 -3
- package/lib/rules-guard.mjs +30 -12
- package/lib/shoot.mjs +3 -3
- package/lib/template-history.json +42 -0
- package/package.json +1 -1
- package/worker/build-id.mjs +1 -1
- package/worker/index.mjs +33 -8
- package/worker/lobby.mjs +2 -1
- package/worker/managed-media.mjs +16 -0
- package/worker/move-drain.mjs +47 -0
- package/worker/read-only.mjs +113 -0
- package/worker/routes.mjs +12 -1
- package/worker/stream-objects.mjs +3 -2
- package/worker/table.mjs +10 -1
- package/worker/version.mjs +1 -1
package/worker/routes.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {drainStatus} from './move-drain.mjs';
|
|
1
2
|
import {lobbyFor} from './lobby-shards.mjs';
|
|
2
3
|
/** Route dispatch; large interfaces and tool runtimes load at their own entry points. */
|
|
3
4
|
import {customRoute} from './custom-routes.mjs';
|
|
@@ -19,6 +20,7 @@ import { isLoungePath, loungeRoutes } from './lounge.mjs';
|
|
|
19
20
|
import { doorPage, serverPage, serversPage } from './site.mjs';
|
|
20
21
|
import { isLocalOrigin, qrSvg } from './qr.mjs';
|
|
21
22
|
import { gateFor } from './gate-routing.mjs';
|
|
23
|
+
import { isReadOnly } from './read-only.mjs';
|
|
22
24
|
|
|
23
25
|
import { SITE_JS, atomFeed, creditsPage, customPage, gameCover, gameLanding, gamesPage, homeLd, homePage, jsonFeed, landingLd, mediaIndexPage, notFoundPage, postPage, postsPage, roomsPage, sectionsOf, songPage, videoPage, watchOf } from './site.mjs';
|
|
24
26
|
import { cookieValues, countVisit, onlyOf, ownerAllowed, playedByGame, playedThisWeek, rangeOf, readStats } from './stats.mjs';
|
|
@@ -32,6 +34,8 @@ import { ldScript, studioNode } from './schema.mjs';
|
|
|
32
34
|
import { licenseOf } from './license.mjs';
|
|
33
35
|
import { isPartsPath, partsRoutes, withPartsBand } from './parts.mjs';
|
|
34
36
|
import { GAME_ID, POST_SLUG, BROWSER_KEY, json, sellingShop, availableShellShop, postsOf, directoryOf, claimOf, named, roomsOf, MEDIA_SLUG, assetWithRange, mediaObject, ASSET_CACHE, heldInR2, mediaRow, chatWho, reportAllowed, notAHomie, gameDocument, gameAsset, shareDaily, mediaBeat, studioTools, serverLive, weekOf, fallback } from './runtime-support.mjs';
|
|
37
|
+
/** Rooms whose admission row (move-drain.mjs) this isolate already wrote. */
|
|
38
|
+
const REGISTERED_ROOMS = new Set();
|
|
35
39
|
export default async function route(request, env, ctx) {
|
|
36
40
|
const url = new URL(request.url);
|
|
37
41
|
const path = url.pathname;
|
|
@@ -156,9 +160,11 @@ export default async function route(request, env, ctx) {
|
|
|
156
160
|
...(shopOf(cat)?.open ? { shop: { open: true, till: shopOf(cat).till, currency: shopOf(cat).currency, page: `${url.origin}/shop/` } } : {}),
|
|
157
161
|
}, 200, { 'access-control-allow-origin': '*' });
|
|
158
162
|
}
|
|
163
|
+
if (path === '/api/move-status' && read) return json(await drainStatus(env,await getAll()),200,{'cache-control':'no-store'});
|
|
159
164
|
if (path === '/api/games') {
|
|
160
165
|
const cat = await getCat();
|
|
161
|
-
|
|
166
|
+
// readOnly: the owner's switch (worker/read-only.mjs), here so a tool can check it without sending a write.
|
|
167
|
+
return json({ studio: cat.studio, readOnly: isReadOnly(env), games: (cat.games ?? []).map(g => g.room?.host === 'server' && !hostedGame(g.id) ? { ...g, rulesError: RULES_WIRING_MESSAGE } : g), live: (await roomsOf(env, cat.games ?? [])).live });
|
|
162
168
|
}
|
|
163
169
|
if (path === '/api/rooms') {
|
|
164
170
|
if(!read)return json({ok:false,error:'method'},405);
|
|
@@ -639,6 +645,11 @@ export default async function route(request, env, ctx) {
|
|
|
639
645
|
// one; none on a kids server, none for an AI or a watcher.
|
|
640
646
|
const pid = !ag && !w && sub === '__net' ? parts.find((x) => x.startsWith('p-'))?.slice(2) : null;
|
|
641
647
|
const badge = pid ? await roomBadge(env, cat, pid, { kids: pol.kids }) : null;
|
|
648
|
+
// Keep an admission registry independent of expiring lobby reports. A later move
|
|
649
|
+
// must inspect even a quiet named room and its durable outboxes.
|
|
650
|
+
// Once per room in this isolate: the row is idempotent, so later sockets to the same room cost no D1 write.
|
|
651
|
+
const roomKey='move-room:'+game+':'+room;
|
|
652
|
+
if(env.DB&&!isReadOnly(env)&&!REGISTERED_ROOMS.has(roomKey)){await env.DB.prepare("INSERT OR IGNORE INTO meta(key,value) VALUES(?1,'1')").bind(roomKey).run();REGISTERED_ROOMS.add(roomKey);}
|
|
642
653
|
const stub = sub === '__net' ? gateFor(env, game, room, max, b) : env.TABLE.get(env.TABLE.idFromName(`${game}/${room}`));
|
|
643
654
|
// Revision 9 (NETPLAY.md sections 22 and 23): the build that is live now (`cur`, always said, empty when the game
|
|
644
655
|
// names none), the build this socket's page was served with (`gv`, from the socket's own address), and the game's
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import {readOnlyEnv} from './read-only.mjs';
|
|
1
2
|
/** Streams use independent namespaces so subscribers never warm matchmaking state. */
|
|
2
3
|
export class AppRecordHub {
|
|
3
|
-
constructor(ctx, env) { this.ctx=ctx; this.env=env; }
|
|
4
|
+
constructor(ctx, env) { this.ctx=ctx; this.env=readOnlyEnv(env); }
|
|
4
5
|
async records(){const {AppRecordStreams}=await import('./app-streams.mjs');return this.streams??=new AppRecordStreams(this.env,this.ctx);}
|
|
5
6
|
async fetch(request) {
|
|
6
7
|
const streams=await this.records();
|
|
@@ -17,7 +18,7 @@ export class AppRecordHub {
|
|
|
17
18
|
async webSocketError(socket){return (await this.records()).closeSocket(socket);}
|
|
18
19
|
}
|
|
19
20
|
export class EventHub {
|
|
20
|
-
constructor(ctx, env) { this.ctx=ctx; this.env=env; }
|
|
21
|
+
constructor(ctx, env) { this.ctx=ctx; this.env=readOnlyEnv(env); }
|
|
21
22
|
async fetch(request) {
|
|
22
23
|
const {EventStreams}=await import('./event-resources.mjs');
|
|
23
24
|
this.streams??=new EventStreams(this.env,this.ctx);
|
package/worker/table.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {holders,regateArgs,verifyControl} from './office.mjs';
|
|
|
22
22
|
|
|
23
23
|
import {json} from './responses.mjs';
|
|
24
24
|
import {catalogue} from './catalogue.mjs';
|
|
25
|
+
import {readOnlyEnv} from './read-only.mjs';
|
|
25
26
|
|
|
26
27
|
import {ROOM_ALARM_MS,ROOM_PAUSE_MS,roomEndAt} from './room-lifecycle.mjs';
|
|
27
28
|
|
|
@@ -29,7 +30,8 @@ export class Table {
|
|
|
29
30
|
constructor(ctx, env) {
|
|
30
31
|
this.ctx = ctx;
|
|
31
32
|
this.watchSockets = new WatchSockets(ctx);
|
|
32
|
-
|
|
33
|
+
// Read-only (worker/read-only.mjs): the room plays on; its D1 writes wait in its outbox or for the next report.
|
|
34
|
+
this.env = readOnlyEnv(env);
|
|
33
35
|
this.room = null;
|
|
34
36
|
this.timer = null;
|
|
35
37
|
this.saved = null;
|
|
@@ -1087,6 +1089,13 @@ export class Table {
|
|
|
1087
1089
|
}
|
|
1088
1090
|
// The back office (worker/office.mjs), from the studio's Worker only: the room as its owner sees it, and the
|
|
1089
1091
|
// owner's signed controls, which this room verifies before it applies one (NETPLAY.md section 15).
|
|
1092
|
+
if (url.pathname === '/__move-status') {
|
|
1093
|
+
await this.flushRoundRecords();await this.flushFunctionEvents();
|
|
1094
|
+
const sql=this.ctx.storage.sql;
|
|
1095
|
+
let pending=0;
|
|
1096
|
+
for(const name of ['round_outbox','function_outbox'])if(sql?.exec("SELECT name FROM sqlite_master WHERE name=?",name).toArray().length)pending+=Number(sql.exec('SELECT COUNT(*) AS n FROM '+name).toArray()[0].n);
|
|
1097
|
+
return json({clients:room.live().length,pending});
|
|
1098
|
+
}
|
|
1090
1099
|
if (url.pathname === '/__facts') return json({ ...room.officeFacts(), gates: [...(this.gateMetrics?.values() ?? [])], durability: this.storageHealth ?? { ok: true }, ...(this.house ? { brains: this.house.facts() } : {}) });
|
|
1091
1100
|
// A chat line as the room keeps it (section 19), for a report: the Worker files the room's own copy, never a reporter's.
|
|
1092
1101
|
if (url.pathname === '/__chat') {
|
package/worker/version.mjs
CHANGED
|
@@ -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.58.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.58.11';
|