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
@@ -0,0 +1,279 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+ import { failure, isRecord, parseCommandRequest } from "./protocol.js";
3
+ const STATE_KEY = "game-state";
4
+ const TIMEOUT_KEY = "decision-timeout";
5
+ /**
6
+ * 1ルームを動かすCloudflare専用runtime。アプリはadapterだけを接続した派生クラスをexportする。
7
+ * RPCとfetchは信頼されたWorker専用。認証・外部リクエスト保護はWorker入口が担当する。
8
+ */
9
+ export class SessionRuntime extends DurableObject {
10
+ /** 所有者や参加者を要求せず、ゲームの初期状態だけを保存する。 */
11
+ async create() {
12
+ return this.ctx.blockConcurrencyWhile(async () => {
13
+ try {
14
+ const exists = await this.ctx.storage.get(STATE_KEY);
15
+ if (exists !== undefined)
16
+ return failure("RoomAlreadyExists");
17
+ const state = this.adapter.game.createInitialState();
18
+ if (state === undefined)
19
+ throw new Error("Initial state must be persistable");
20
+ await this.ctx.storage.transaction(async (txn) => { await txn.put(STATE_KEY, state); });
21
+ return { ok: true, roomId: this.ctx.id.toString() };
22
+ }
23
+ catch {
24
+ this.#report("create");
25
+ return failure("InternalError");
26
+ }
27
+ });
28
+ }
29
+ /** HTTP等からの読み取り。公開内容はprojectが決め、WS接続資格は要求しない。 */
30
+ async getView(actor) {
31
+ if (!actor || typeof actor.actorId !== "string" || actor.actorId.length === 0)
32
+ return failure("InvalidRequest");
33
+ return this.ctx.blockConcurrencyWhile(async () => {
34
+ try {
35
+ const state = await this.ctx.storage.get(STATE_KEY);
36
+ if (state === undefined)
37
+ return failure("RoomNotFound");
38
+ return { ok: true, view: this.adapter.game.project(state, actor.actorId) };
39
+ }
40
+ catch {
41
+ this.#report("view");
42
+ return failure("InternalError");
43
+ }
44
+ });
45
+ }
46
+ /** アプリが認証済みActorと、必要なら外部取得済み情報を渡す入口。 */
47
+ async dispatchActor(actor, command) {
48
+ if (!actor || typeof actor.actorId !== "string" || actor.actorId.length === 0)
49
+ return failure("InvalidRequest");
50
+ return this.#dispatchCommand(command, { origin: "actor", actorId: actor.actorId });
51
+ }
52
+ /** webhook等の認証を済ませたサーバー専用入口。外部本文からoriginをコピーしない。 */
53
+ async dispatchSystem(command) {
54
+ return this.#dispatchCommand(command, { origin: "system" });
55
+ }
56
+ /** 全Commandが通る唯一の経路。Alarmの予約確認も同じ直列化区間で行う。 */
57
+ async #dispatchCommand(command, context, options = {}) {
58
+ let effects = [];
59
+ const outcome = await this.ctx.blockConcurrencyWhile(async () => {
60
+ try {
61
+ let consumed;
62
+ if (options.alarm) {
63
+ const reservation = await this.ctx.storage.get(TIMEOUT_KEY);
64
+ if (!reservation)
65
+ return { ok: true };
66
+ if (Date.now() < reservation.deadline) {
67
+ await this.ctx.storage.setAlarm(reservation.deadline);
68
+ return { ok: true };
69
+ }
70
+ if (!this.adapter.timeout)
71
+ throw new Error("Timeout adapter missing");
72
+ command = this.adapter.timeout.command(reservation.decisionId);
73
+ consumed = reservation.decisionId;
74
+ }
75
+ const state = await this.ctx.storage.get(STATE_KEY);
76
+ if (state === undefined)
77
+ return failure("RoomNotFound");
78
+ if (options.fromSocket && (context.origin !== "actor" || !this.adapter.canConnect(state, context.actorId))) {
79
+ return failure("NotRoomMember");
80
+ }
81
+ const result = this.adapter.game.handleCommand(state, command, context);
82
+ if (!result.ok)
83
+ return { ok: false, error: { kind: "game", detail: result.error } };
84
+ if (result.state === undefined)
85
+ throw new Error("State must be persistable");
86
+ const external = [];
87
+ const alarms = [];
88
+ for (const effect of result.effects) {
89
+ const alarm = this.adapter.timeout?.effect(effect) ?? null;
90
+ if (alarm === null)
91
+ external.push(effect);
92
+ else {
93
+ if (!alarm.decisionId || (alarm.type !== "schedule" && alarm.type !== "cancel")
94
+ || (alarm.type === "schedule" && !Number.isFinite(alarm.deadline)))
95
+ throw new Error("Invalid alarm effect");
96
+ alarms.push(alarm);
97
+ }
98
+ }
99
+ await this.ctx.storage.transaction(async (txn) => {
100
+ await txn.put(STATE_KEY, result.state);
101
+ if (consumed !== undefined) {
102
+ const current = await txn.get(TIMEOUT_KEY);
103
+ if (current?.decisionId === consumed) {
104
+ await txn.delete(TIMEOUT_KEY);
105
+ await txn.deleteAlarm();
106
+ }
107
+ }
108
+ for (const alarm of alarms) {
109
+ if (alarm.type === "schedule") {
110
+ await txn.put(TIMEOUT_KEY, { decisionId: alarm.decisionId, deadline: alarm.deadline });
111
+ await txn.setAlarm(alarm.deadline);
112
+ }
113
+ else {
114
+ const current = await txn.get(TIMEOUT_KEY);
115
+ if (current?.decisionId === alarm.decisionId) {
116
+ await txn.delete(TIMEOUT_KEY);
117
+ await txn.deleteAlarm();
118
+ }
119
+ }
120
+ }
121
+ });
122
+ // 保存後の配信失敗で、確定したCommandを失敗に戻さない。
123
+ effects = external;
124
+ this.#broadcast(result.state);
125
+ return { ok: true };
126
+ }
127
+ catch {
128
+ this.#report("command");
129
+ return failure("InternalError");
130
+ }
131
+ });
132
+ // 排他区間を出てから呼ぶ。外部取得中も次のCommandを受け付ける。
133
+ if (effects.length)
134
+ this.#deliver(effects);
135
+ return outcome;
136
+ }
137
+ /** Alarm失敗は例外としてCloudflareの有限回リトライへ返す。 */
138
+ async alarm() {
139
+ const result = await this.#dispatchCommand(undefined, { origin: "system" }, { alarm: true });
140
+ if (!result.ok)
141
+ throw new Error("Timeout command failed");
142
+ }
143
+ /** 認証済みWorkerからだけ呼ぶ内部WS入口。一般HTTP Commandの転送は受け付けない。 */
144
+ async fetch(request) {
145
+ if (request.method !== "GET" || new URL(request.url).pathname !== "/connect"
146
+ || request.headers.get("upgrade")?.toLowerCase() !== "websocket") {
147
+ return Response.json(failure("InvalidRequest"), { status: 400 });
148
+ }
149
+ const actorId = request.headers.get("x-def-game-actor-id");
150
+ if (!actorId)
151
+ return Response.json(failure("InvalidRequest"), { status: 400 });
152
+ return this.ctx.blockConcurrencyWhile(async () => {
153
+ try {
154
+ const state = await this.ctx.storage.get(STATE_KEY);
155
+ if (state === undefined)
156
+ return Response.json(failure("RoomNotFound"), { status: 404 });
157
+ if (!this.adapter.canConnect(state, actorId))
158
+ return Response.json(failure("NotRoomMember"), { status: 403 });
159
+ const viewState = this.adapter.game.project(state, actorId);
160
+ const pair = new WebSocketPair();
161
+ pair[1].serializeAttachment({ actorId });
162
+ this.ctx.acceptWebSocket(pair[1]);
163
+ this.#send(pair[1], { type: "ViewStateEvent", viewState });
164
+ return new Response(null, { status: 101, webSocket: pair[0] });
165
+ }
166
+ catch {
167
+ this.#report("connection");
168
+ return Response.json(failure("InternalError"), { status: 500 });
169
+ }
170
+ });
171
+ }
172
+ /** WSの自己申告を実行元にせず、接続に保存したActorでdispatchする。 */
173
+ async webSocketMessage(socket, raw) {
174
+ const request = parseCommandRequest(raw);
175
+ let command = null;
176
+ try {
177
+ if (request)
178
+ command = this.adapter.webSocket.parseCommand(request.command);
179
+ }
180
+ catch { /* 入力を拒否 */ }
181
+ if (!request || command === null || command === undefined) {
182
+ this.#send(socket, { type: "ProtocolErrorEvent", error: failure("InvalidRequest").error });
183
+ return;
184
+ }
185
+ const actorId = this.#actor(socket);
186
+ const result = actorId === null ? failure("NotRoomMember")
187
+ : await this.#dispatchCommand(command, { origin: "actor", actorId }, { fromSocket: true });
188
+ this.#send(socket, { type: "GameCommandResponse", requestId: request.requestId, ...result });
189
+ }
190
+ /** 保存済みの状態は変更せず、接続ごとに公開可能なViewだけ配る。 */
191
+ #broadcast(state) {
192
+ try {
193
+ for (const socket of this.ctx.getWebSockets()) {
194
+ try {
195
+ const actorId = this.#actor(socket);
196
+ if (actorId === null || !this.adapter.canConnect(state, actorId)) {
197
+ this.#close(socket, 1008);
198
+ continue;
199
+ }
200
+ this.#send(socket, { type: "ViewStateEvent", viewState: this.adapter.game.project(state, actorId) });
201
+ }
202
+ catch {
203
+ this.#report("view");
204
+ this.#close(socket);
205
+ }
206
+ }
207
+ }
208
+ catch {
209
+ this.#report("view");
210
+ }
211
+ }
212
+ /** handlerの結果を新しいSystem Commandとして返す。永続配送・自動再試行はしない。 */
213
+ #deliver(effects) {
214
+ this.ctx.waitUntil((async () => {
215
+ for (const effect of effects) {
216
+ let feedback;
217
+ try {
218
+ if (!this.adapter.executeEffect)
219
+ throw new Error("Effect handler missing");
220
+ feedback = await this.adapter.executeEffect(effect, { env: this.env, roomId: this.ctx.id.toString() });
221
+ }
222
+ catch {
223
+ this.#report("effect");
224
+ continue;
225
+ }
226
+ if (feedback !== undefined) {
227
+ try {
228
+ const result = await this.dispatchSystem(feedback.command);
229
+ if (!result.ok)
230
+ this.#report("effect-feedback");
231
+ }
232
+ catch {
233
+ this.#report("effect-feedback");
234
+ }
235
+ }
236
+ }
237
+ })());
238
+ }
239
+ #actor(socket) {
240
+ try {
241
+ const attachment = socket.deserializeAttachment();
242
+ return isRecord(attachment) && typeof attachment.actorId === "string" ? attachment.actorId : null;
243
+ }
244
+ catch {
245
+ return null;
246
+ }
247
+ }
248
+ #send(socket, message) {
249
+ try {
250
+ socket.send(JSON.stringify(message));
251
+ }
252
+ catch {
253
+ this.#report("view");
254
+ this.#close(socket);
255
+ }
256
+ }
257
+ #close(socket, code = 1011) {
258
+ try {
259
+ socket.close(code, "Connection closed");
260
+ }
261
+ catch { /* 既に切断済み */ }
262
+ }
263
+ #report(phase) {
264
+ const failure = { phase, roomId: this.ctx.id.toString() };
265
+ try {
266
+ if (this.adapter.onError)
267
+ this.adapter.onError(failure);
268
+ else
269
+ console.error("DefGame runtime failure", failure);
270
+ }
271
+ catch {
272
+ console.error("DefGame diagnostic handler failed", failure);
273
+ }
274
+ }
275
+ webSocketClose(socket, code) {
276
+ this.#close(socket, code === 1005 || code === 1006 || code === 1015 ? 1000 : code);
277
+ }
278
+ webSocketError(socket) { this.#close(socket); }
279
+ }
@@ -0,0 +1,99 @@
1
+ import type { GameDefinition } from "../game-definition.js";
2
+ /** ゲームが所有する型をまとめる。runtimeはStateの内部構造を解釈しない。 */
3
+ export interface GameTypes {
4
+ state: unknown;
5
+ actorCommand: unknown;
6
+ systemCommand: unknown;
7
+ view: unknown;
8
+ effect: unknown;
9
+ error: unknown;
10
+ }
11
+ /** アプリが本人確認を済ませた実行元。型そのものに認証能力はない。 */
12
+ export interface VerifiedActor {
13
+ readonly actorId: string;
14
+ }
15
+ /** ゲームの拒否とインフラ側の拒否を区別する。内部例外はペイロードへ含めない。 */
16
+ export type RuntimeError = {
17
+ readonly kind: "runtime";
18
+ readonly code: "RoomNotFound" | "RoomAlreadyExists" | "InvalidRequest" | "NotRoomMember" | "InternalError";
19
+ };
20
+ export type CommandResult<Error> = {
21
+ readonly ok: true;
22
+ } | {
23
+ readonly ok: false;
24
+ readonly error: RuntimeError | {
25
+ readonly kind: "game";
26
+ readonly detail: Error;
27
+ };
28
+ };
29
+ export type CreateRoomResult = {
30
+ readonly ok: true;
31
+ readonly roomId: string;
32
+ } | {
33
+ readonly ok: false;
34
+ readonly error: RuntimeError;
35
+ };
36
+ /** 保存状態そのものではなく、ゲームがActorに公開するViewを返す。 */
37
+ export type GetViewResult<View> = {
38
+ readonly ok: true;
39
+ readonly view: View;
40
+ } | {
41
+ readonly ok: false;
42
+ readonly error: RuntimeError;
43
+ };
44
+ /** 状態と一緒に確定する、1つの現在decisionに対する予約・解除。 */
45
+ export type TimeoutEffect = {
46
+ readonly type: "schedule";
47
+ readonly decisionId: string;
48
+ readonly deadline: number;
49
+ } | {
50
+ readonly type: "cancel";
51
+ readonly decisionId: string;
52
+ };
53
+ /** 失敗した区間を通知する。秘密の状態・Command・Effectを自動ログに含めない。 */
54
+ export interface RuntimeFailure {
55
+ readonly roomId: string;
56
+ readonly phase: "create" | "command" | "effect" | "effect-feedback" | "view" | "connection";
57
+ }
58
+ /** 純粋なゲームと、アプリ固有の入力・外部処理を共通runtimeへ接続する。 */
59
+ export interface GameAdapter<Env, T extends GameTypes> {
60
+ readonly game: GameDefinition<T["state"], T["actorCommand"] | T["systemCommand"], string, T["view"], T["effect"], T["error"]>;
61
+ readonly webSocket: {
62
+ /** クライアントが指定してよい入力だけCommandにする。手番等の検証はゲームに残す。 */
63
+ readonly parseCommand: (input: unknown) => T["actorCommand"] | null;
64
+ };
65
+ readonly canConnect: (state: T["state"], actorId: string) => boolean;
66
+ readonly timeout?: {
67
+ readonly effect: (effect: T["effect"]) => TimeoutEffect | null;
68
+ readonly command: (decisionId: string) => T["systemCommand"];
69
+ };
70
+ /** 保存後のbest-effort処理。結果はruntimeがSystem Commandとして再度dispatchする。 */
71
+ readonly executeEffect?: (effect: T["effect"], context: {
72
+ readonly env: Env;
73
+ readonly roomId: string;
74
+ }) => Promise<void | {
75
+ readonly command: T["systemCommand"];
76
+ }>;
77
+ /** 同期の診断フック。例外はruntimeが隔離する。配送・再試行の仕組みではない。 */
78
+ readonly onError?: (failure: RuntimeFailure) => void;
79
+ }
80
+ /** WebSocketの共通envelope。actorIdやoriginはクライアントから受け取らない。 */
81
+ export interface GameCommandRequest<Command> {
82
+ readonly type: "GameCommandRequest";
83
+ readonly requestId: string;
84
+ readonly command: Command;
85
+ }
86
+ export type GameCommandResponse<Error> = CommandResult<Error> & {
87
+ readonly type: "GameCommandResponse";
88
+ readonly requestId: string;
89
+ };
90
+ export interface ViewStateEvent<View> {
91
+ readonly type: "ViewStateEvent";
92
+ readonly viewState: View;
93
+ }
94
+ export interface ProtocolErrorEvent {
95
+ readonly type: "ProtocolErrorEvent";
96
+ readonly error: RuntimeError;
97
+ }
98
+ export type ServerMessage<View, Error> = GameCommandResponse<Error> | ViewStateEvent<View> | ProtocolErrorEvent;
99
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/cloudflare/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Worker や simulator が確定したコマンドの実行元。
3
+ * actorId は認証済みの主体を表し、接続 ID ではない。
4
+ * 外部リクエストの自己申告をそのまま使わず、system は信頼できる実行側だけが指定する。
5
+ */
6
+ export type CommandContext<ActorId> = {
7
+ readonly origin: "actor";
8
+ readonly actorId: ActorId;
9
+ } | {
10
+ readonly origin: "system";
11
+ };
12
+ /**
13
+ * 成功時は次の安定状態と副作用の宣言、失敗時はゲーム上の拒否理由を返す。
14
+ * 実行側は成功した状態を保存してから外部 Effect を処理する。
15
+ * 同じストレージ内の Alarm 予約などは、状態と原子的に保存できる。
16
+ */
17
+ export type TransitionResult<State, Effect, Error> = {
18
+ readonly ok: true;
19
+ readonly state: State;
20
+ readonly effects: readonly Effect[];
21
+ } | {
22
+ readonly ok: false;
23
+ readonly error: Error;
24
+ };
25
+ /**
26
+ * 通信・永続化から独立したゲームそのものの契約。
27
+ * State の構造や入力待ちの表現は各ゲームが定義し、kernel は task queue を要求しない。
28
+ */
29
+ export interface GameDefinition<State, Command, ActorId, View, Effect, Error> {
30
+ /** Session や接続の情報に依存しない、ゲームの初期状態を生成する。 */
31
+ createInitialState(): State;
32
+ /**
33
+ * 実行元とコマンドを再検証し、次の外部入力を待てる安定状態まで遷移する。
34
+ * 入力 state を変更せず、拒否時は error のみを返す。外部 I/O は行わない。
35
+ * system 起点でも、そのコマンドが現在の状態で有効かを検証する。
36
+ */
37
+ handleCommand(state: State, command: Command, context: CommandContext<ActorId>): TransitionResult<State, Effect, Error>;
38
+ /**
39
+ * state を変更せず、Actor に公開できる情報と availableActions を持つ View を生成する。
40
+ * availableActions は表示補助であり、コマンド実行時の検証を省略する根拠にはしない。
41
+ */
42
+ project(state: State, actorId: ActorId): View;
43
+ }
44
+ //# sourceMappingURL=game-definition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-definition.d.ts","sourceRoot":"","sources":["../../src/game-definition.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,cAAc,CAAC,OAAO,IAC5B;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEpC;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAC3C;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC,GACC;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpD;;;GAGG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK;IACxE,0CAA0C;IAC1C,kBAAkB,IAAI,KAAK,CAAC;IAE5B;;;;OAIG;IACH,aAAa,CACT,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,GACjC,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAE1C;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACjD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ {"type":"module"}