def-game 5.1.0-timeout.0 → 6.1.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 (50) hide show
  1. package/README.md +11 -114
  2. package/bin/def-game.cjs +8 -124
  3. package/bin/init-worker.cjs +48 -0
  4. package/dist/worker/cloudflare/index.d.ts +5 -0
  5. package/dist/worker/cloudflare/index.d.ts.map +1 -0
  6. package/dist/worker/cloudflare/index.js +2 -0
  7. package/dist/worker/cloudflare/protocol.d.ts +11 -0
  8. package/dist/worker/cloudflare/protocol.d.ts.map +1 -0
  9. package/dist/worker/cloudflare/protocol.js +24 -0
  10. package/dist/worker/cloudflare/room-client.d.ts +28 -0
  11. package/dist/worker/cloudflare/room-client.d.ts.map +1 -0
  12. package/dist/worker/cloudflare/room-client.js +21 -0
  13. package/dist/worker/cloudflare/session-runtime.d.ts +27 -0
  14. package/dist/worker/cloudflare/session-runtime.d.ts.map +1 -0
  15. package/dist/worker/cloudflare/session-runtime.js +279 -0
  16. package/dist/worker/cloudflare/types.d.ts +99 -0
  17. package/dist/worker/cloudflare/types.d.ts.map +1 -0
  18. package/dist/worker/cloudflare/types.js +1 -0
  19. package/dist/worker/game-definition.d.ts +44 -0
  20. package/dist/worker/game-definition.d.ts.map +1 -0
  21. package/dist/worker/game-definition.js +1 -0
  22. package/dist/worker/package.json +1 -0
  23. package/docs/design-philosophy.md +410 -0
  24. package/docs/runtime-api.md +490 -0
  25. package/package.json +33 -7
  26. package/templates/starter/AGENTS.md +15 -0
  27. package/templates/starter/README.md +18 -0
  28. package/templates/starter/gitignore +5 -0
  29. package/templates/starter/package.json +15 -0
  30. package/templates/starter/public/app.js +93 -0
  31. package/templates/starter/public/index.html +37 -0
  32. package/templates/starter/scripts/dev-auth.mjs +40 -0
  33. package/templates/starter/scripts/dev.mjs +16 -0
  34. package/templates/starter/src/env.ts +10 -0
  35. package/templates/starter/src/external/decks.ts +15 -0
  36. package/templates/starter/src/game/definition.ts +79 -0
  37. package/templates/starter/src/game/types.ts +44 -0
  38. package/templates/starter/src/game-adapter.ts +29 -0
  39. package/templates/starter/src/index.ts +88 -0
  40. package/templates/starter/src/room.ts +9 -0
  41. package/templates/starter/tsconfig.json +8 -0
  42. package/templates/starter/wrangler.jsonc +14 -0
  43. package/templates/worker/env.ts +0 -9
  44. package/templates/worker/index.ts +0 -84
  45. package/templates/worker/runtime/game-adapter.ts +0 -15
  46. package/templates/worker/runtime/parse.ts +0 -14
  47. package/templates/worker/session.ts +0 -100
  48. package/templates/worker/timeout/runtime/game-adapter.ts +0 -18
  49. package/templates/worker/timeout/session.ts +0 -152
  50. /package/templates/{worker → starter/src}/auth.ts +0 -0
package/README.md CHANGED
@@ -1,121 +1,18 @@
1
- # def-game V5
1
+ # def-game
2
2
 
3
- ターン制ゲームのルールを通信・永続化から独立させる、TypeScript の最小 contract とメモリ上の simulator です。
3
+ ゲームの状態遷移をCommandに集約し、外部処理をEffectで接続するTypeScriptライブラリです。
4
+ 共通runtimeが部屋の保存・WebSocket・Alarmを扱い、ゲームルールと認証・外部サービスとの接続はアプリ側で定義します。
4
5
 
5
- ```sh
6
- npm install def-game@^5
7
- ```
8
-
9
- ## 公開 API
10
-
11
- - `GameDefinition`: 初期状態の生成、コマンドによる状態遷移、Actor 別の projection。
12
- - `TransitionResult`: 成功時の次状態と Effect、または失敗理由。
13
- - `CommandContext`: Actor と system の実行元の区別。
14
- - `GameSimulator`: 同じゲーム定義へコマンドを順に流し、状態・View・Effect を検証する実行器。
15
-
16
- 型の詳細と契約は [game-definition.ts](src/game-definition.ts)、実行器は [game-simulator.ts](src/game-simulator.ts) を参照してください。
17
- [シナリオテスト](test/game-simulator.test.cjs) に、2人の入力待ちと system timeout を扱う実行例があります。
18
-
19
- ## 責務
20
-
21
- 各ゲームは GameState と Command を定義し、1コマンドで次の外部入力を待てる安定状態まで遷移させます。入力状態は変更せず、失敗時はエラーだけを返します。現在の入力待ちはゲーム自身の状態として表現します。
22
-
23
- 接続、認証、永続化、時刻の取得、Alarm、外部 I/O は実行側の責務です。実行側が検証済みの Actor または system を context に指定します。リクエストに書かれた実行元をそのまま信用してはいけません。ゲーム側でも実行元と現在の状態に対してコマンドを検証します。
24
-
25
- Effect は副作用の宣言です。本番の実行側は次状態を保存してから Effect を実行します。simulator は Effect を実行せず、遷移結果に含めて返します。
26
-
27
- projection は秘密情報を除いた Actor 別の View と availableActions を生成します。availableActions は表示補助であり、コマンド実行時の検証を置き換えません。
28
-
29
- simulator の getState は検証用の全状態への参照です。呼び出し側も状態を直接変更しないでください。deep clone、freeze、失敗時の rollback は行いません。クライアントへ公開する情報には getView を使用します。
30
-
31
- ## V4 からの移行
32
-
33
- V5 は破壊的変更です。GameRule、GameEngine、TaskQueue に関する型、旧通信型と helper は削除しています。V4 を利用する既存ゲームは移植が完了するまで V4 を使用してください。
34
-
35
- - 旧ライフサイクルを GameDefinition の初期化・コマンド処理・projection に移します。
36
- - task queue に未来の処理を保存する代わりに、ゲーム固有の状態で現在の入力待ちを表現します。
37
- - 参加・開始などもゲームの Command として扱います。
38
- - 旧 GameEngine の代わりに GameSimulator でコマンド列を実行します。
39
-
40
- ## Worker generator(5.0.1)
41
-
42
- Hono Worker、SQLite-backed Durable Object、waki.work JWT 認証、Hibernation WebSocket、保存・配信処理を生成します。GameDefinition の契約変更はありません。標準構成は timeout / Effect 実行なしです。開発版では下記の timeout オプションを追加しています。
43
-
44
- server package に `def-game.worker.json` を作ります。全パスはこの設定ファイルのディレクトリ基準です。
45
-
46
- ```json
47
- {
48
- "outputDir": "src/worker",
49
- "adapter": "src/worker/game-adapter.ts",
50
- "entry": "src/index.ts",
51
- "wranglerConfig": "wrangler.jsonc",
52
- "name": "my-game",
53
- "assets": "../client/public",
54
- "compatibilityDate": "2026-09-12"
55
- }
56
- ```
57
-
58
- ```sh
59
- npm install --save-dev def-game@5.0.1 wrangler @cloudflare/workers-types
60
- npm install hono jose
61
- npx def-game generate-worker --config def-game.worker.json
62
- npx def-game generate-worker --config def-game.worker.json --check
63
- ```
64
-
65
- 生成対象は `outputDir` 内の `index.ts`、`session.ts`、`auth.ts`、`env.ts`、`runtime/parse.ts`、`runtime/game-adapter.ts` と、`entry`、`wranglerConfig` の計8ファイルです。Hono / jose は利用側の runtime dependency で、generator 自体に実行時依存はありません。
66
-
67
- 生成コードは Node.js 24.20.0、TypeScript 5.6.3、Hono 4.13.7、jose 6.2.12、Wrangler 4.131.1、`@cloudflare/workers-types` 5.20260911.1 の環境で、ビルド・型チェック・HTTP / WebSocket / Durable Object の通信テストを確認しています。
68
-
69
- ゲーム側は既存ファイルとして `adapter` を用意し、次を export します。このファイル、domain、shared は生成・上書きしません。
70
-
71
- - `gameAdapter`: 生成される `GameAdapter<State, Command, View, Error>` に適合するオブジェクト。
72
- - `gameAdapter.game`: ゲーム自身の GameDefinition(Effect は `never`)。
73
- - `parseCreate(input)` / `parseJoin(input)` / `parseCommand(input)`: 未検証入力から command を返す関数。形式不正なら null。WS の関数へ渡るのは envelope 内の command だけです。
74
- - `canConnect(state, actorId)`: 接続・配信を許可するかの判定。command の認可は GameDefinition が再検証します。
75
- - 型の export: `State`、`ActorId`(string)、`ServerMessage`、`ProtocolError`、`PublicError`、`CreateSessionResponse`、`JoinSessionResponse`。
76
-
77
- 公開通信は `GameCommandRequest { requestId, command }`、`GameCommandResponse { requestId, ok, error? }`、`ViewStateEvent { viewState }`、`ProtocolErrorEvent { error }` の type で区別します。HTTP 応答は `CreateSessionResponse` / `JoinSessionResponse` の type と、成功時 `ok: true, sessionId`、失敗時 `ok: false, error` を持ちます。ProtocolError の code は `AuthenticationRequired` / `SessionNotFound` / `NotSessionMember` / `InvalidRequest` / `InternalError`。PublicError はそれとゲームエラーの union です。
6
+ ## Getting Started
78
7
 
79
- 認証は Cookie `__token` を JWKS / ES256 / issuer / audience / exp で検証し、UUID の sub を ActorId として使います。Static Assets も認証必須。ログイン誘導・refresh は client の責務です。
80
-
81
- 出力が同一なら変更しません。既存ファイルと差分がある場合は書き込み前に停止します。確認後に `--force` を付けると生成対象8ファイルだけを更新します。`--check` は欠落・差分を非ゼロ終了で報告し、ファイルを変更しません。生成ファイルは Git 管理し、修正は generator / 設定側へ戻してください。Wrangler 設定も全体が生成対象なので、独自 bindings や migrations の追加には generator の対応が必要です。
82
-
83
- CLI は dependencies、package scripts、tsconfig を書き換えません。利用側で Worker の型と Hono / jose を設定し、生成された entry を Wrangler から実行します。CLI には Node.js 20 以降が必要です。
84
-
85
- ## 開発と公開
8
+ Node.js 22以降が必要です。v6.1.0の利用手順です。
86
9
 
87
10
  ```sh
88
- npm ci
89
- npm run typecheck
90
- npm test
91
- npm pack
11
+ npx def-game@6.1.0 init-worker --directory my-game --name my-game
12
+ cd my-game
13
+ npm install
14
+ npm run dev
92
15
  ```
93
16
 
94
- 開発時のテストには Node.js 20 以降を使用してください。npm test はビルドと Node 標準のシナリオテストを実行します。
95
-
96
- npm pack / npm publish の前に prepack で型チェック・ビルド・テストが実行されます。build は dist を作り直すため、削除した旧 API の生成物が混入しません。公開対象は dist、bin、templates、package.json、README、LICENSE です。
97
-
98
- main にレビュー済みの変更を反映した後、公開する version と認証アカウントを確認し、検証済みのパッケージを npm publish で公開します。
99
-
100
-
101
- ## Decision timeout(5.1.0-timeout.0 開発版)
102
-
103
- 生成設定に `"timeout": true` を追加すると、DO Alarm と system command 配送を生成します。未指定または false なら従来の構成です。開発版は npm 未公開です。
104
-
105
- `GameAdapter<State, Command, View, Error, Effect>` に次の任意の接続点を設定します。
106
-
107
- ```ts
108
- timeout: {
109
- effect: (effect) => /* { type: "schedule", decisionId: string, deadline: number }
110
- または { type: "cancel", decisionId: string } */,
111
- command: (decisionId) => /* ゲーム固有の system command */,
112
- }
113
- ```
114
-
115
- ゲームは新しい decision で再利用しない ID と期限を発行します。同じ decision 内の部分完了では Effect を出しません。default action・pending actors・bot policy はゲーム側に残します。runtime は State を解釈せず、1つの現在予約を保存します。独立した複数の同時 decision のスケジューラーではありません。
116
-
117
- 状態・予約・Alarm は同じ storage transaction で確定します。Alarm は保存済み予約から system command を生成し、古い発火が新しい予約を期限前に処理しないよう再予約します。domain も ID・期限を検証してください。期限を迎えた予約は成功した遷移と同じ transaction で消費し、次の予約があれば置き換えます。失敗は例外として伝播し Cloudflare の有限回の再試行に委ねます。無制限の再試行・外部 Effect の配送は含みません。
118
-
119
- このオプションで扱う Effect は timeout の予約・解除のみです。外部通知と異なり、Alarm は状態と原子的に保存するローカルな永続化処理です。外部サービス呼び出しは transaction 内に追加しないでください。
120
-
121
- 実行環境の検証: Node.js 24、TypeScript 5.6、Hono 4.13、jose 6.2、Wrangler 4.131、Cloudflare Vitest plugin 1.1。標準構成・任意構成の生成一致を generator テストで確認しています。
17
+ `http://127.0.0.1:8787`を開いてください。
18
+ 実装前に[設計思想](docs/design-philosophy.md)を読み、接続方法は[runtime API・初期生成の使い方](docs/runtime-api.md)を参照してください。
package/bin/def-game.cjs CHANGED
@@ -1,133 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
-
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
3
  const { version } = require('../package.json');
7
-
8
- const help = `def-game ${version}
9
- Usage: def-game generate-worker --config <file.json> [--force | --check]
10
-
11
- Generate a Hono Worker, SQLite Durable Object, waki.work authentication and Wrangler config.
12
- --force Replace differing output files (never edits the game adapter).
13
- --check Verify generated files match without writing anything.
14
- Node.js 20+ is required. Set timeout: true in the config to generate decision timeout scheduling.`;
15
-
16
- function relativeImport(from, target) {
17
- let value = path.relative(path.dirname(from), target).split(path.sep).join('/').replace(/\.ts$/, '.js');
18
- if (!value.startsWith('.')) value = './' + value;
19
- return JSON.stringify(value);
20
- }
21
-
22
4
  function main(args) {
23
- if (args.length === 0 || args[0] === '--help') return console.log(help);
24
- if (args[0] === '--version') return console.log(version);
25
- if (Number(process.versions.node.split('.')[0]) < 20) throw new Error('Node.js 20+ is required');
26
- if (args.shift() !== 'generate-worker') throw new Error('Unknown command. Use --help.');
27
- let configPath;
28
- let force = false;
29
- let check = false;
30
- while (args.length) {
31
- const arg = args.shift();
32
- if (arg === '--config' && !configPath && args[0] && !args[0].startsWith('--')) configPath = args.shift();
33
- else if (arg === '--force' && !force) force = true;
34
- else if (arg === '--check' && !check) check = true;
35
- else throw new Error(`Unknown or repeated option: ${arg}`);
36
- }
37
- if (!configPath || (force && check)) throw new Error('Provide --config and at most one of --force / --check');
38
- configPath = path.resolve(configPath);
39
- const root = path.dirname(configPath);
40
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
41
- if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Config must be an object');
42
- const allowed = new Set(['outputDir', 'adapter', 'entry', 'wranglerConfig', 'name', 'assets', 'compatibilityDate', 'timeout']);
43
- if (config.timeout !== undefined && typeof config.timeout !== 'boolean') throw new Error('timeout must be boolean');
44
- for (const key of Object.keys(config)) if (!allowed.has(key)) throw new Error(`Unknown config key: ${key}`);
45
- const required = (key) => {
46
- if (typeof config[key] !== 'string' || !config[key].trim() || config[key].includes('\0')) throw new Error(`Invalid config: ${key}`);
47
- return config[key];
48
- };
49
- const inside = (value) => {
50
- if (path.isAbsolute(value)) throw new Error(`Path must be relative: ${value}`);
51
- const resolved = path.resolve(root, value);
52
- const relative = path.relative(root, resolved);
53
- if (!relative || relative === '..' || relative.startsWith('..' + path.sep)) throw new Error(`Path must stay inside config directory: ${value}`);
54
- // Do not follow existing symlinks when overwriting output or locating the adapter.
55
- let current = root;
56
- for (const part of relative.split(path.sep)) {
57
- current = path.join(current, part);
58
- if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error(`Symlink is not supported: ${current}`);
59
- }
60
- return resolved;
61
- };
62
- const outputDir = inside(required('outputDir'));
63
- const adapter = inside(required('adapter'));
64
- if (!/\.(ts|js)$/.test(adapter) || !fs.statSync(adapter).isFile()) throw new Error('adapter must be an existing .ts or .js file');
65
- const entry = inside(required('entry'));
66
- if (!entry.endsWith('.ts')) throw new Error('entry must end with .ts');
67
- const wrangler = inside(required('wranglerConfig'));
68
- if (!wrangler.endsWith('.jsonc')) throw new Error('wranglerConfig must end with .jsonc');
69
- const name = required('name');
70
- if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name)) throw new Error('Invalid Worker name');
71
- const assets = required('assets');
72
- if (path.isAbsolute(assets)) throw new Error('assets must be relative to the config directory');
73
- const date = required('compatibilityDate');
74
- if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(date)) || new Date(date).toISOString().slice(0, 10) !== date) {
75
- throw new Error('Invalid compatibilityDate');
76
- }
77
-
78
- const banner = `// Generated by def-game ${version}. Do not edit; change the generator or its config.\n`;
79
- const files = new Map();
80
- const add = (file, text) => {
81
- if (files.has(file) || file === adapter || file === configPath) throw new Error(`Output path collision: ${file}`);
82
- inside(path.relative(root, file));
83
- files.set(file, banner + text);
84
- };
85
- for (const file of ['index.ts', 'session.ts', 'auth.ts', 'env.ts', 'runtime/parse.ts', 'runtime/game-adapter.ts']) {
86
- const target = path.join(outputDir, file);
87
- const template = config.timeout && ['session.ts', 'runtime/game-adapter.ts'].includes(file) ? path.join('timeout', file) : file;
88
- const source = fs.readFileSync(path.join(__dirname, '../templates/worker', template), 'utf8');
89
- add(target, source.replaceAll('__ADAPTER_IMPORT__', relativeImport(target, adapter)));
90
- }
91
- add(entry, `export { default, SessionDurableObject } from ${relativeImport(entry, path.join(outputDir, 'index.ts'))};\n`);
92
- const fromWrangler = (target) => path.relative(path.dirname(wrangler), target).split(path.sep).join('/');
93
- add(wrangler, JSON.stringify({
94
- name,
95
- main: fromWrangler(entry),
96
- compatibility_date: date,
97
- assets: { directory: fromWrangler(path.resolve(root, assets)), binding: 'ASSETS', run_worker_first: true },
98
- durable_objects: { bindings: [{ name: 'SESSIONS', class_name: 'SessionDurableObject' }] },
99
- migrations: [{ tag: 'v1', new_sqlite_classes: ['SessionDurableObject'] }],
100
- vars: {
101
- AUTH_ISSUER: 'https://auth.waki.work', AUTH_AUDIENCE: 'waki.work',
102
- AUTH_JWKS_URL: 'https://auth.waki.work/.well-known/jwks.json',
103
- },
104
- }, null, 2) + '\n');
5
+ if (!args.length || args[0] === '--help') {
6
+ return console.log(`def-game ${version}
7
+ Usage: def-game init-worker --directory <new-directory> --name <worker-name>
105
8
 
106
- // Preflight every output before the first write, so conflicts cannot leave half-generated code.
107
- const changed = [];
108
- for (const [file, contents] of files) {
109
- const exists = fs.existsSync(file);
110
- if (exists && !fs.statSync(file).isFile()) throw new Error(`Output is not a file: ${file}`);
111
- if (exists && fs.readFileSync(file, 'utf8') === contents) continue;
112
- if (exists && !force && !check) throw new Error(`Refusing to overwrite ${file}; review the changes and use --force`);
113
- changed.push([file, contents]);
114
- }
115
- if (check) {
116
- if (changed.length) throw new Error(`Generated files differ or are missing:\n${changed.map(([file]) => path.relative(root, file)).join('\n')}`);
117
- return console.log(`Verified ${files.size} generated files (def-game ${version})`);
9
+ Create an app-owned v6 starter. Existing directories are never overwritten.
10
+ Node.js 22+ is required.`);
118
11
  }
119
- for (const [file, contents] of changed) {
120
- fs.mkdirSync(path.dirname(file), { recursive: true });
121
- const temp = file + `.def-game-${process.pid}.tmp`;
122
- try {
123
- fs.writeFileSync(temp, contents, { flag: 'wx' });
124
- fs.renameSync(temp, file);
125
- } finally {
126
- if (fs.existsSync(temp)) fs.unlinkSync(temp);
127
- }
128
- }
129
- console.log(`Generated ${files.size} files; changed ${changed.length} (def-game ${version})`);
12
+ if (args[0] === '--version') return console.log(version);
13
+ if (args.shift() !== 'init-worker') throw new Error('Unknown command. Use --help.');
14
+ return require('./init-worker.cjs')(args);
130
15
  }
131
-
132
16
  try { main(process.argv.slice(2)); }
133
17
  catch (error) { console.error(`def-game: ${error.message}`); process.exitCode = 1; }
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { version } = require('../package.json');
5
+
6
+ module.exports = function initWorker(args) {
7
+ if (args.length === 1 && args[0] === '--help') {
8
+ return console.log('Usage: def-game init-worker --directory <new-directory> --name <worker-name>');
9
+ }
10
+ if (Number(process.versions.node.split('.')[0]) < 22) throw new Error('init-worker requires Node.js 22+');
11
+ const options = {};
12
+ while (args.length) {
13
+ const key = args.shift();
14
+ if (!['--directory', '--name'].includes(key) || options[key] || !args[0] || args[0].startsWith('--')) {
15
+ throw new Error(`Unknown, repeated or incomplete option: ${key}`);
16
+ }
17
+ options[key] = args.shift();
18
+ }
19
+ if (!options['--directory'] || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(options['--name'] || '')) {
20
+ throw new Error('Provide --directory and --name (1–63 lowercase letters, digits or hyphens)');
21
+ }
22
+ const target = path.resolve(options['--directory']);
23
+ // Initial generation has no update/force mode: all output belongs to the app.
24
+ if (fs.existsSync(target) || (() => { try { fs.lstatSync(target); return true; } catch { return false; } })()) {
25
+ throw new Error(`Destination already exists: ${target}; choose a new directory`);
26
+ }
27
+ fs.mkdirSync(path.dirname(target), { recursive: true });
28
+ const stage = fs.mkdtempSync(path.join(path.dirname(target), '.def-game-init-'));
29
+ try {
30
+ fs.cpSync(path.join(__dirname, '../templates/starter'), stage, { recursive: true });
31
+ const packagePath = path.join(stage, 'package.json');
32
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
33
+ pkg.name = options['--name'];
34
+ pkg.dependencies['def-game'] = version;
35
+ fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
36
+ const configPath = path.join(stage, 'wrangler.jsonc');
37
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
38
+ config.name = options['--name'];
39
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
40
+ fs.renameSync(path.join(stage, 'gitignore'), path.join(stage, '.gitignore'));
41
+ // Reserve the destination exclusively before copying; never replace a racing writer.
42
+ fs.mkdirSync(target);
43
+ fs.cpSync(stage, target, { recursive: true, force: false, errorOnExist: true });
44
+ } finally {
45
+ fs.rmSync(stage, { recursive: true, force: true });
46
+ }
47
+ console.log(`Created v6 starter at ${target}\nInstall dependencies, then run npm run dev. See README.md for local alpha installation.`);
48
+ };
@@ -0,0 +1,5 @@
1
+ export { SessionRuntime } from "./session-runtime.js";
2
+ export { getRoom, getSystemRoom } from "./room-client.js";
3
+ export type { RoomClient } from "./room-client.js";
4
+ export type * from "./types.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cloudflare/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC1D,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,mBAAmB,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { SessionRuntime } from "./session-runtime.js";
2
+ export { getRoom, getSystemRoom } from "./room-client.js";
@@ -0,0 +1,11 @@
1
+ import type { GameCommandRequest, RuntimeError } from "./types.js";
2
+ /** 配列やnullをオブジェクト入力として扱わない。 */
3
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
4
+ /** envelopeだけを読む。ゲーム固有の入力はadapterのWS parserが検証する。 */
5
+ export declare function parseCommandRequest(raw: string | ArrayBuffer): GameCommandRequest<unknown> | null;
6
+ /** runtimeの拒否理由を共通形式にする。 */
7
+ export declare function failure(code: RuntimeError["code"]): {
8
+ ok: false;
9
+ error: RuntimeError;
10
+ };
11
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/cloudflare/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEnE,+BAA+B;AAC/B,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED,qDAAqD;AACrD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,IAAI,CASjG;AAED,4BAA4B;AAC5B,wBAAgB,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,CAEtF"}
@@ -0,0 +1,24 @@
1
+ /** 配列やnullをオブジェクト入力として扱わない。 */
2
+ export function isRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ /** envelopeだけを読む。ゲーム固有の入力はadapterのWS parserが検証する。 */
6
+ export function parseCommandRequest(raw) {
7
+ if (typeof raw !== "string" || new TextEncoder().encode(raw).length > 64 * 1024)
8
+ return null;
9
+ try {
10
+ const value = JSON.parse(raw);
11
+ if (!isRecord(value) || value.type !== "GameCommandRequest" || typeof value.requestId !== "string"
12
+ || value.requestId.length === 0 || value.requestId.length > 128
13
+ || !Object.prototype.hasOwnProperty.call(value, "command"))
14
+ return null;
15
+ return { type: "GameCommandRequest", requestId: value.requestId, command: value.command };
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /** runtimeの拒否理由を共通形式にする。 */
22
+ export function failure(code) {
23
+ return { ok: false, error: { kind: "runtime", code } };
24
+ }
@@ -0,0 +1,28 @@
1
+ import type { CommandResult, CreateRoomResult, GetViewResult, VerifiedActor } from "./types.js";
2
+ /** DOの信頼されたRPC面。アプリは外部リクエストを直接この面へ転送しない。 */
3
+ interface RoomTransport<ActorCommand, SystemCommand, Error, View> {
4
+ create(): Promise<CreateRoomResult>;
5
+ getView(actor: VerifiedActor): Promise<GetViewResult<View>>;
6
+ dispatchActor(actor: VerifiedActor, command: ActorCommand): Promise<CommandResult<Error>>;
7
+ dispatchSystem(command: SystemCommand): Promise<CommandResult<Error>>;
8
+ fetch(request: Request): Promise<Response>;
9
+ }
10
+ /** 通常のアプリ操作向け参照。System入口を含めない。 */
11
+ export interface RoomClient<ActorCommand, Error, View = unknown> {
12
+ create(): Promise<CreateRoomResult>;
13
+ getView(actor: VerifiedActor): Promise<GetViewResult<View>>;
14
+ dispatchActor(actor: VerifiedActor, command: ActorCommand): Promise<CommandResult<Error>>;
15
+ connect(actor: VerifiedActor): Promise<Response>;
16
+ }
17
+ /** 対象DOを選び、認証済みActor向けのAPIだけを返す。IDの解決はアプリが行う。 */
18
+ export declare function getRoom<A, S, E, V>(namespace: {
19
+ get(id: DurableObjectId): RoomTransport<A, S, E, V>;
20
+ }, id: DurableObjectId): RoomClient<A, E, V>;
21
+ /** 認証・権限確認済みのwebhookやマッチングからSystem Commandを渡す参照。 */
22
+ export declare function getSystemRoom<A, S, E, V>(namespace: {
23
+ get(id: DurableObjectId): RoomTransport<A, S, E, V>;
24
+ }, id: DurableObjectId): {
25
+ dispatchSystem(command: S): Promise<CommandResult<E>>;
26
+ };
27
+ export {};
28
+ //# sourceMappingURL=room-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-client.d.ts","sourceRoot":"","sources":["../../../src/cloudflare/room-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhG,4CAA4C;AAC5C,UAAU,aAAa,CAAC,YAAY,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI;IAC9D,MAAM,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACpC,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,aAAa,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1F,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5C;AAED,kCAAkC;AAClC,MAAM,WAAW,UAAU,CAAC,YAAY,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO;IAC7D,MAAM,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACpC,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,aAAa,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1F,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAClD;AAED,iDAAiD;AACjD,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAChC,SAAS,EAAE;IAAE,GAAG,CAAC,EAAE,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CAAE,EAAE,EAAE,EAAE,eAAe,GACtF,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAarB;AAED,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EACtC,SAAS,EAAE;IAAE,GAAG,CAAC,EAAE,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CAAE,EAAE,EAAE,EAAE,eAAe,GACtF;IAAE,cAAc,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;CAAE,CAG3D"}
@@ -0,0 +1,21 @@
1
+ /** 対象DOを選び、認証済みActor向けのAPIだけを返す。IDの解決はアプリが行う。 */
2
+ export function getRoom(namespace, id) {
3
+ const stub = namespace.get(id);
4
+ return {
5
+ create: () => stub.create(),
6
+ getView: actor => stub.getView(actor),
7
+ dispatchActor: (actor, command) => stub.dispatchActor(actor, command),
8
+ connect: actor => {
9
+ if (!actor || typeof actor.actorId !== "string" || !actor.actorId)
10
+ throw new Error("Verified actor required");
11
+ return stub.fetch(new Request("https://def-game.internal/connect", {
12
+ headers: { upgrade: "websocket", "x-def-game-actor-id": actor.actorId },
13
+ }));
14
+ },
15
+ };
16
+ }
17
+ /** 認証・権限確認済みのwebhookやマッチングからSystem Commandを渡す参照。 */
18
+ export function getSystemRoom(namespace, id) {
19
+ const stub = namespace.get(id);
20
+ return { dispatchSystem: command => stub.dispatchSystem(command) };
21
+ }
@@ -0,0 +1,27 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+ import type { CommandResult, CreateRoomResult, GetViewResult, GameAdapter, GameTypes, VerifiedActor } from "./types.js";
3
+ /**
4
+ * 1ルームを動かすCloudflare専用runtime。アプリはadapterだけを接続した派生クラスをexportする。
5
+ * RPCとfetchは信頼されたWorker専用。認証・外部リクエスト保護はWorker入口が担当する。
6
+ */
7
+ export declare abstract class SessionRuntime<Env, T extends GameTypes> extends DurableObject<Env> {
8
+ #private;
9
+ protected abstract readonly adapter: GameAdapter<Env, T>;
10
+ /** 所有者や参加者を要求せず、ゲームの初期状態だけを保存する。 */
11
+ create(): Promise<CreateRoomResult>;
12
+ /** HTTP等からの読み取り。公開内容はprojectが決め、WS接続資格は要求しない。 */
13
+ getView(actor: VerifiedActor): Promise<GetViewResult<T["view"]>>;
14
+ /** アプリが認証済みActorと、必要なら外部取得済み情報を渡す入口。 */
15
+ dispatchActor(actor: VerifiedActor, command: T["actorCommand"]): Promise<CommandResult<T["error"]>>;
16
+ /** webhook等の認証を済ませたサーバー専用入口。外部本文からoriginをコピーしない。 */
17
+ dispatchSystem(command: T["systemCommand"]): Promise<CommandResult<T["error"]>>;
18
+ /** Alarm失敗は例外としてCloudflareの有限回リトライへ返す。 */
19
+ alarm(): Promise<void>;
20
+ /** 認証済みWorkerからだけ呼ぶ内部WS入口。一般HTTP Commandの転送は受け付けない。 */
21
+ fetch(request: Request): Promise<Response>;
22
+ /** WSの自己申告を実行元にせず、接続に保存したActorでdispatchする。 */
23
+ webSocketMessage(socket: WebSocket, raw: string | ArrayBuffer): Promise<void>;
24
+ webSocketClose(socket: WebSocket, code: number): void;
25
+ webSocketError(socket: WebSocket): void;
26
+ }
27
+ //# sourceMappingURL=session-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-runtime.d.ts","sourceRoot":"","sources":["../../../src/cloudflare/session-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAgD,aAAa,EAAE,MAAM,YAAY,CAAC;AAMtK;;;GAGG;AACH,8BAAsB,cAAc,CAAC,GAAG,EAAE,CAAC,SAAS,SAAS,CAAE,SAAQ,aAAa,CAAC,GAAG,CAAC;;IACvF,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAEzD,oCAAoC;IAC9B,MAAM,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAgBzC,iDAAiD;IAC3C,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IActE,wCAAwC;IAClC,aAAa,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAKzG,oDAAoD;IAC9C,cAAc,CAAC,OAAO,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAgFrF,0CAA0C;IACpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,uDAAuD;IACjD,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAyBhD,8CAA8C;IACxC,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAiEnF,cAAc,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAGrD,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;CACxC"}