@joystyk/launcher 0.0.0 → 0.2.0

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +245 -2
  3. package/dist/browser/daily-prefetch.d.ts +8 -0
  4. package/dist/browser/daily-prefetch.js +32 -0
  5. package/dist/browser/daily-prefetch.js.map +1 -0
  6. package/dist/browser/index.d.ts +3 -0
  7. package/dist/browser/index.js +4 -0
  8. package/dist/browser/rooms.d.ts +2 -0
  9. package/dist/browser/rooms.js +49 -0
  10. package/dist/browser/rooms.js.map +1 -0
  11. package/dist/browser/services.d.ts +127 -0
  12. package/dist/browser/services.js +95 -0
  13. package/dist/browser/services.js.map +1 -0
  14. package/dist/browser/stage.d.ts +28 -0
  15. package/dist/browser/stage.js +1195 -0
  16. package/dist/browser/stage.js.map +1 -0
  17. package/dist/fixture/engine.d.ts +60 -0
  18. package/dist/fixture/engine.js +164 -0
  19. package/dist/fixture/engine.js.map +1 -0
  20. package/dist/fixture/index.d.ts +52 -0
  21. package/dist/fixture/index.js +63 -0
  22. package/dist/fixture/index.js.map +1 -0
  23. package/dist/fixture/module.d.ts +2 -0
  24. package/dist/fixture/module.js +135 -0
  25. package/dist/fixture/module.js.map +1 -0
  26. package/dist/fixture/probe.d.ts +10 -0
  27. package/dist/fixture/probe.js +19 -0
  28. package/dist/fixture/probe.js.map +1 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.js +4 -0
  31. package/dist/launcher.d.ts +190 -0
  32. package/dist/launcher.js +4614 -0
  33. package/dist/launcher.js.map +1 -0
  34. package/dist/model.d.ts +431 -0
  35. package/dist/model.js +566 -0
  36. package/dist/model.js.map +1 -0
  37. package/dist/report-problem.d.ts +39 -0
  38. package/dist/report-problem.js +359 -0
  39. package/dist/report-problem.js.map +1 -0
  40. package/dist/report.d.ts +18 -0
  41. package/dist/report.js +32 -0
  42. package/dist/report.js.map +1 -0
  43. package/dist/room-drivers.d.ts +26 -0
  44. package/dist/room-drivers.js +92 -0
  45. package/dist/room-drivers.js.map +1 -0
  46. package/dist/safe-point.d.ts +33 -0
  47. package/dist/safe-point.js +44 -0
  48. package/dist/safe-point.js.map +1 -0
  49. package/dist/save-slots.d.ts +6 -0
  50. package/dist/save-slots.js +18 -0
  51. package/dist/save-slots.js.map +1 -0
  52. package/dist/theme.d.ts +35 -0
  53. package/dist/theme.js +111 -0
  54. package/dist/theme.js.map +1 -0
  55. package/package.json +67 -11
@@ -0,0 +1 @@
1
+ {"version":3,"file":"services.js","names":[],"sources":["../../src/browser/services.ts"],"sourcesContent":["// What the stage needs from the page: the games it can load, the player's storage, the\n// system settings preferences resolve against, the session runners' ports and fresh seeds.\n// The app provides them once, above the launcher, with `provideLauncherServices`; tests\n// provide their own (memory storage, fast AI). Platform packages never import a game, so the\n// game list always comes from the app (ARCHITECTURE.md).\nimport type { GameEntry, RngState } from '@joystyk/contracts';\nimport { seedState } from '@joystyk/kit/rng';\nimport type { Clock, FrameScheduler, Reporter, SocketFactory, TokenStore, Visibility } from '@joystyk/session';\nimport { browserPorts } from '@joystyk/session/browser';\nimport type { PlayerStorage, Storage, StorageNotice, SystemEnvironment } from '@joystyk/storage';\n\n/** How the app loads one game: its entry, and a worker for heavy AI if the game has one. */\nexport type GameLoader = {\n /** The game's `entry.ts` (manifest plus lazy loaders), usually a dynamic import. */\n readonly entry: () => Promise<GameEntry<HTMLElement>>;\n /**\n * For a manifest with `ai.heavy`: a worker running the game's `jobs/ai.worker.ts`\n * (`new Worker(new URL(…, import.meta.url), { type: 'module' })`). Without it the runner\n * plays heavy AI inline and reports it.\n */\n readonly aiWorker?: () => Worker;\n /**\n * For an engine that requests jobs a local game may run (`where: 'either'`, such as Greed's\n * throws): a fresh worker for one job of `kind`, running the game's `jobs/<kind>.worker.ts`.\n * Without it the runner fails those jobs, and the engine decides what that means.\n */\n readonly jobWorker?: (kind: string) => Worker;\n};\n\n/** The games a page can launch, by id. */\nexport type GameLoaders = Readonly<Record<string, GameLoader>>;\n\n/** The guest, with their storage. */\nexport type PlayerContext = {\n readonly playerId: string;\n readonly displayName: string;\n readonly storage: PlayerStorage;\n /** The device's storage, which the daily puzzle's content cache and results use (`@joystyk/daily/browser`). */\n readonly device?: Storage;\n};\n\n/**\n * Where the daily mode finds published puzzles, and the wall clock that says which UTC day it\n * is (D-46). Tests pass a fake `fetch` and a fixed clock.\n */\nexport type DailyServices = {\n /** Default \"/daily\", the site's published-content path (content-pipeline.md, \"Delivery\"). */\n readonly baseUrl?: string;\n readonly fetch?: (url: string, init?: RequestInit) => Promise<Response>;\n /** Epoch milliseconds. Default `Date.now`. */\n readonly now?: () => number;\n};\n\n/** The system settings `resolvePreferences` falls back on, now and on change. */\nexport interface SystemSource {\n get(): SystemEnvironment;\n subscribe(listener: (environment: SystemEnvironment) => void): () => void;\n}\n\n/** A room server answer: the value, or why not, worded for players. */\nexport type RoomAnswer<T> = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly message: string };\n\n/**\n * How the stage reaches the room server (D-48). The default (`browser/rooms.ts`, loaded with\n * the stage) uses this page's origin, where Traefik routes `/api/rooms` and `/ws` to the room\n * service; tests pass their own.\n */\nexport interface RoomServices {\n /** `POST /api/rooms`: a new room with the creator in seat 0, and a notice when the name was replaced. */\n create(\n gameId: string,\n name: string,\n ): Promise<RoomAnswer<{ readonly room: string; readonly token: string; readonly notice: string | null }>>;\n /** `GET /api/rooms/:code`: which game the room plays. */\n find(code: string): Promise<RoomAnswer<{ readonly gameId: string }>>;\n readonly connect: SocketFactory;\n readonly tokens: TokenStore;\n /** A fresh action ID: 8–40 URL-safe characters. */\n actionId(): string;\n /** The invite link: this game page with `?room=CODE`. */\n inviteLink(code: string): string;\n}\n\nexport type RunnerPorts = {\n readonly clock: Clock;\n readonly visibility: Visibility;\n readonly frames: FrameScheduler;\n readonly report: Reporter;\n};\n\nexport interface LauncherServices {\n readonly games: GameLoaders;\n /** Opens storage and the guest identity; the same promise every time. */\n openPlayer(): Promise<PlayerContext>;\n readonly system: SystemSource;\n readonly ports: RunnerPorts;\n /** A fresh, unpredictable seed for a new game (and, separately, its AI). */\n newSeed(): RngState;\n /** Storage problems worded for players (a full disk, private browsing). Returns how to stop. */\n onNotice(listener: (message: string) => void): () => void;\n /** The pause before an inline AI moves, so players can follow. Default 500 ms. */\n readonly aiDelayMs?: number;\n /** Deep-freeze snapshots (development). Default true. */\n readonly freeze?: boolean;\n /** The room server for Online with friends. Default: this page's origin. */\n readonly rooms?: RoomServices;\n /** The daily puzzle's content and clock. Default: this page's `/daily/` and the device clock. */\n readonly daily?: DailyServices;\n}\n\n// --- Providing and finding them ------------------------------------------------------------------\n\nconst provided = new WeakMap<Node, LauncherServices>();\n\n/** Makes `services` available to every launcher below `target` (the document, or a test container). */\nexport const provideLauncherServices = (target: Node, services: LauncherServices): (() => void) => {\n provided.set(target, services);\n return () => {\n if (provided.get(target) === services) provided.delete(target);\n };\n};\n\n/** The services of the nearest provider above `from`, crossing shadow roots, or null. */\nexport const findLauncherServices = (from: Node): LauncherServices | null => {\n let node: Node | null = from;\n while (node !== null) {\n const found = provided.get(node);\n if (found !== undefined) return found;\n node = node.parentNode ?? (node instanceof ShadowRoot ? node.host : null);\n }\n return null;\n};\n\n// --- Building them -------------------------------------------------------------------------------\n\n/** The system settings from media queries, following changes. */\nexport const mediaSystem = (view: Window = window): SystemSource => {\n const motion = view.matchMedia('(prefers-reduced-motion: reduce)');\n const dark = view.matchMedia('(prefers-color-scheme: dark)');\n const get = (): SystemEnvironment => ({\n prefersReducedMotion: motion.matches,\n prefersDark: dark.matches,\n locale: view.navigator.language,\n });\n return {\n get,\n subscribe: (listener) => {\n const onChange = (): void => listener(get());\n motion.addEventListener('change', onChange);\n dark.addEventListener('change', onChange);\n return () => {\n motion.removeEventListener('change', onChange);\n dark.removeEventListener('change', onChange);\n };\n },\n };\n};\n\n/** A seed from 128 bits of the browser's cryptographic randomness. */\nexport const cryptoSeed = (): RngState => seedState(crypto.randomUUID());\n\nexport type LauncherServiceOptions = {\n readonly games: GameLoaders;\n /** Opens storage. Default: IndexedDB (`openBrowserStorage`), falling back to memory. */\n readonly openStorage?: (onNotice: (notice: StorageNotice) => void) => Promise<Storage>;\n readonly system?: SystemSource;\n readonly ports?: Partial<RunnerPorts>;\n readonly newSeed?: () => RngState;\n readonly aiDelayMs?: number;\n readonly freeze?: boolean;\n readonly rooms?: RoomServices;\n readonly daily?: DailyServices;\n};\n\n/**\n * The page's services. Storage and identity open on the first game, not on page load, and\n * their code loads then too.\n */\nexport const createLauncherServices = (options: LauncherServiceOptions): LauncherServices => {\n const listeners = new Set<(message: string) => void>();\n const notify = (notice: StorageNotice): void => {\n for (const listener of [...listeners]) listener(notice.message);\n };\n let player: Promise<PlayerContext> | null = null;\n const openPlayer = async (): Promise<PlayerContext> => {\n const storage = await (\n options.openStorage ??\n (async (onNotice) => (await import('@joystyk/storage/browser')).openBrowserStorage({ onNotice }))\n )(notify);\n const { createBrowserIdentity } = await import('@joystyk/identity/browser');\n const profile = await createBrowserIdentity(storage).current();\n return {\n playerId: profile.id,\n displayName: profile.displayName,\n storage: storage.player(profile.id),\n device: storage,\n };\n };\n return {\n games: options.games,\n openPlayer: () => {\n player ??= openPlayer().catch((error: unknown) => {\n player = null;\n throw error;\n });\n return player;\n },\n system: options.system ?? mediaSystem(),\n ports: { ...browserPorts(), ...options.ports },\n newSeed: options.newSeed ?? cryptoSeed,\n onNotice: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n ...(options.aiDelayMs === undefined ? {} : { aiDelayMs: options.aiDelayMs }),\n ...(options.freeze === undefined ? {} : { freeze: options.freeze }),\n ...(options.rooms === undefined ? {} : { rooms: options.rooms }),\n ...(options.daily === undefined ? {} : { daily: options.daily }),\n };\n};\n"],"mappings":";;;AAgHA,IAAM,2BAAW,IAAI,QAAgC;;AAGrD,IAAa,2BAA2B,QAAc,aAA6C;CACjG,SAAS,IAAI,QAAQ,QAAQ;CAC7B,aAAa;EACX,IAAI,SAAS,IAAI,MAAM,MAAM,UAAU,SAAS,OAAO,MAAM;CAC/D;AACF;;AAGA,IAAa,wBAAwB,SAAwC;CAC3E,IAAI,OAAoB;CACxB,OAAO,SAAS,MAAM;EACpB,MAAM,QAAQ,SAAS,IAAI,IAAI;EAC/B,IAAI,UAAU,KAAA,GAAW,OAAO;EAChC,OAAO,KAAK,eAAe,gBAAgB,aAAa,KAAK,OAAO;CACtE;CACA,OAAO;AACT;;AAKA,IAAa,eAAe,OAAe,WAAyB;CAClE,MAAM,SAAS,KAAK,WAAW,kCAAkC;CACjE,MAAM,OAAO,KAAK,WAAW,8BAA8B;CAC3D,MAAM,aAAgC;EACpC,sBAAsB,OAAO;EAC7B,aAAa,KAAK;EAClB,QAAQ,KAAK,UAAU;CACzB;CACA,OAAO;EACL;EACA,YAAY,aAAa;GACvB,MAAM,iBAAuB,SAAS,IAAI,CAAC;GAC3C,OAAO,iBAAiB,UAAU,QAAQ;GAC1C,KAAK,iBAAiB,UAAU,QAAQ;GACxC,aAAa;IACX,OAAO,oBAAoB,UAAU,QAAQ;IAC7C,KAAK,oBAAoB,UAAU,QAAQ;GAC7C;EACF;CACF;AACF;;AAGA,IAAa,mBAA6B,UAAU,OAAO,WAAW,CAAC;;;;;AAmBvE,IAAa,0BAA0B,YAAsD;CAC3F,MAAM,4BAAY,IAAI,IAA+B;CACrD,MAAM,UAAU,WAAgC;EAC9C,KAAK,MAAM,YAAY,CAAC,GAAG,SAAS,GAAG,SAAS,OAAO,OAAO;CAChE;CACA,IAAI,SAAwC;CAC5C,MAAM,aAAa,YAAoC;EACrD,MAAM,UAAU,OACd,QAAQ,gBACP,OAAO,cAAc,MAAM,OAAO,4BAAA,CAA6B,mBAAmB,EAAE,SAAS,CAAC,GAAA,CAC/F,MAAM;EACR,MAAM,EAAE,0BAA0B,MAAM,OAAO;EAC/C,MAAM,UAAU,MAAM,sBAAsB,OAAO,CAAC,CAAC,QAAQ;EAC7D,OAAO;GACL,UAAU,QAAQ;GAClB,aAAa,QAAQ;GACrB,SAAS,QAAQ,OAAO,QAAQ,EAAE;GAClC,QAAQ;EACV;CACF;CACA,OAAO;EACL,OAAO,QAAQ;EACf,kBAAkB;GAChB,WAAW,WAAW,CAAC,CAAC,OAAO,UAAmB;IAChD,SAAS;IACT,MAAM;GACR,CAAC;GACD,OAAO;EACT;EACA,QAAQ,QAAQ,UAAU,YAAY;EACtC,OAAO;GAAE,GAAG,aAAa;GAAG,GAAG,QAAQ;EAAM;EAC7C,SAAS,QAAQ,WAAW;EAC5B,WAAW,aAAa;GACtB,UAAU,IAAI,QAAQ;GACtB,aAAa,UAAU,OAAO,QAAQ;EACxC;EACA,GAAI,QAAQ,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,QAAQ,UAAU;EAC1E,GAAI,QAAQ,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,QAAQ,OAAO;EACjE,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAC9D,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;CAChE;AACF"}
@@ -0,0 +1,28 @@
1
+ import { type GameEntry, type Json, type SessionSnapshot } from '@joystyk/contracts';
2
+ import { type RemoteSnapshot } from '@joystyk/session';
3
+ import { type FrameState, type RoomFrame, type RoomRequest, type StageControl } from '../model.js';
4
+ export declare const STAGE_TAG = "jy-game-stage";
5
+ /** What the frame needs from a snapshot, as plain data. */
6
+ export declare const frameOf: (snapshot: SessionSnapshot) => FrameState;
7
+ /** What the lobby and the bar need from a room snapshot, as plain data. */
8
+ export declare const roomFrameOf: (snapshot: RemoteSnapshot<Json, Json>, context: {
9
+ readonly code: string;
10
+ readonly link: string;
11
+ readonly manifest: GameEntry["manifest"];
12
+ readonly now: number;
13
+ }) => RoomFrame;
14
+ /** The stage element. Set `control`; listen for `gyral-output` events. */
15
+ export declare class GameStage extends HTMLElement {
16
+ #private;
17
+ get control(): StageControl | null;
18
+ set control(value: StageControl | null);
19
+ /** A one-shot command from the launcher; the remote room is the authority. */
20
+ roomRequest(request: RoomRequest): void;
21
+ connectedCallback(): void;
22
+ disconnectedCallback(): void;
23
+ }
24
+ declare global {
25
+ interface HTMLElementTagNameMap {
26
+ 'jy-game-stage': GameStage;
27
+ }
28
+ }