@mpgd/game-runtime 0.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.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/actions/index.d.ts +75 -0
- package/dist/actions/index.js +321 -0
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +3 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +122 -0
- package/dist/observers.d.ts +3 -0
- package/dist/observers.js +22 -0
- package/dist/phaser/index.d.ts +26 -0
- package/dist/phaser/index.js +286 -0
- package/dist/platform/index.d.ts +25 -0
- package/dist/platform/index.js +120 -0
- package/dist/ui/index.d.ts +33 -0
- package/dist/ui/index.js +197 -0
- package/docs/phaser.md +106 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 imjlk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Game runtime
|
|
2
|
+
|
|
3
|
+
`@mpgd/game-runtime` coordinates gameplay execution with owned block tokens.
|
|
4
|
+
Its headless entrypoints have no Phaser, DOM, network, platform SDK, timer, or polling imports.
|
|
5
|
+
It does not change engine state, replace score-oriented `GameSession`, or alter
|
|
6
|
+
`PlatformGateway`. Engine and UI consumers apply the requested state separately.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createGameExecutionController } from '@mpgd/game-runtime';
|
|
10
|
+
|
|
11
|
+
const runtime = createGameExecutionController({ onListenerError: reportError });
|
|
12
|
+
const settings = runtime.acquireBlock({
|
|
13
|
+
reason: 'settings', channels: ['simulation', 'gameplay-input'],
|
|
14
|
+
});
|
|
15
|
+
const background = runtime.acquireBlock({
|
|
16
|
+
reason: 'background', channels: ['simulation', 'gameplay-input', 'audio'],
|
|
17
|
+
});
|
|
18
|
+
background.release(); // Settings still blocks simulation and gameplay input.
|
|
19
|
+
settings.release(); // Those channels may now resume.
|
|
20
|
+
runtime.destroy(); // Terminal; this never requests gameplay resume.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Channel | Request |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `simulation` | Stop gameplay simulation updates |
|
|
26
|
+
| `gameplay-input` | Block gameplay input, leaving pause/resume UI usable |
|
|
27
|
+
| `rendering` | Suppress gameplay rendering according to the engine binding policy |
|
|
28
|
+
| `audio` | Mute gameplay audio through an explicit audio sink |
|
|
29
|
+
|
|
30
|
+
Each channel is blocked while any token includes it. Equal reasons create
|
|
31
|
+
independent tokens. Only the returned token can release its block; diagnostic
|
|
32
|
+
IDs are local to a controller and cannot be used to release anything. Release
|
|
33
|
+
and unsubscribe are idempotent. Keep an additional token for user-confirmed
|
|
34
|
+
resume; the core supplies no countdown or automatic resume policy.
|
|
35
|
+
|
|
36
|
+
`getSnapshot()` returns the same reference until a successful acquisition,
|
|
37
|
+
first release, or first destruction. Each increments `version` once, including
|
|
38
|
+
changes to token diagnostics that leave the effective channel flags unchanged.
|
|
39
|
+
Snapshots, channel flags, token lists, token information, and channel arrays
|
|
40
|
+
are frozen. Caller arrays are copied; no consumer object is frozen.
|
|
41
|
+
|
|
42
|
+
`subscribe` does not emit initially: subscribe, then read `getSnapshot()`.
|
|
43
|
+
Listeners run in registration order. Each notification round captures its
|
|
44
|
+
snapshot and recipient list. Reentrant state changes are immediate but their
|
|
45
|
+
notifications queue behind the current round. Newly registered listeners only
|
|
46
|
+
receive future rounds; unsubscribed listeners are skipped even in a captured
|
|
47
|
+
round. Read the callback argument for that round's state, since `getSnapshot()`
|
|
48
|
+
may already reflect a reentrant change. Duplicate registrations are independent.
|
|
49
|
+
|
|
50
|
+
Synchronous exceptions and rejected listener promises go to optional
|
|
51
|
+
`onListenerError`. Promises are observed without awaiting them. Errors from
|
|
52
|
+
that hook are consumed; no global logging or transport is installed. Listeners
|
|
53
|
+
must not produce an unbounded cycle of state changes.
|
|
54
|
+
|
|
55
|
+
`destroy` is terminal and idempotent. It clears owned blocks, emits a terminal
|
|
56
|
+
snapshot with every channel blocked, and removes listeners. Destruction also
|
|
57
|
+
supersedes outstanding active notification rounds, preventing a stale resume
|
|
58
|
+
notification after shutdown. Late token releases are no-ops; new acquisitions
|
|
59
|
+
and subscriptions throw. Destroying this coordination object does not terminate
|
|
60
|
+
the game process. Consumers must check `status` before applying engine controls.
|
|
61
|
+
|
|
62
|
+
## Installation and entrypoints
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
pnpm add @mpgd/game-runtime
|
|
66
|
+
# Only when using the Phaser binding:
|
|
67
|
+
pnpm add phaser@^4.2.0
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Import | Purpose |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `@mpgd/game-runtime` | Reason-scoped execution controller |
|
|
73
|
+
| `@mpgd/game-runtime/ui` | Scoped snapshots, commands and events |
|
|
74
|
+
| `@mpgd/game-runtime/platform` | Injected lifecycle source binding |
|
|
75
|
+
| `@mpgd/game-runtime/actions` | Purchase and rewarded-ad coordination |
|
|
76
|
+
| `@mpgd/game-runtime/phaser` | Optional gameplay scene binding |
|
|
77
|
+
|
|
78
|
+
Phaser is an optional peer dependency. The root and headless subpaths do not
|
|
79
|
+
re-export the Phaser binding or reference its declarations, so headless consumers
|
|
80
|
+
can use them without installing Phaser or enabling DOM types. The action subpath
|
|
81
|
+
uses the published `@mpgd/game-services/operations` types; it does not load the
|
|
82
|
+
service implementation. Supply the existing game-services client explicitly.
|
|
83
|
+
|
|
84
|
+
The [Phaser binding guide](./docs/phaser.md) describes scene ownership and cleanup.
|
|
85
|
+
This package replaces the kit's two unpublished runtime previews with one public
|
|
86
|
+
package. Internal users of `@mpgd/phaser-game-runtime` should switch to
|
|
87
|
+
`@mpgd/game-runtime/phaser`. Existing generated games do not gain a dependency or
|
|
88
|
+
require migration automatically.
|
|
89
|
+
|
|
90
|
+
Releases use Sampo changesets and npm Trusted Publishing from
|
|
91
|
+
`imjlk/mpgd-kit`'s `.github/workflows/release.yml` after initial npm registration.
|
|
92
|
+
|
|
93
|
+
Contributor validation: `pnpm --dir packages/game-runtime test`,
|
|
94
|
+
`node tools/run-ttsx.mjs tools/package/build-packages.ts @mpgd/game-runtime`,
|
|
95
|
+
and `node packages/game-runtime/test/dist-import.mjs`. The package test also
|
|
96
|
+
installs packed tarballs in an isolated consumer, checks headless declarations
|
|
97
|
+
without DOM types, and checks the optional binding with Phaser declarations.
|
|
98
|
+
|
|
99
|
+
## Scoped UI bridge
|
|
100
|
+
|
|
101
|
+
`@mpgd/game-runtime/ui` is a separate, headless entrypoint. Importing the root
|
|
102
|
+
execution controller does not load the UI bridge. The bridge separates persistent
|
|
103
|
+
snapshots, user-intent commands, and one-time events. Multiple command handlers
|
|
104
|
+
are allowed and run in registration order; dispatch is not a success result or
|
|
105
|
+
proof that a purchase or reward was granted. Events have no replay or history.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { createGameUiBridge } from '@mpgd/game-runtime/ui';
|
|
109
|
+
|
|
110
|
+
const bridge = createGameUiBridge<
|
|
111
|
+
{ count: number }, { type: 'refresh' }, { type: 'refreshed' }
|
|
112
|
+
>({ initialSnapshot: { count: 0 }, onListenerError: reportError });
|
|
113
|
+
const screen = bridge.createScope();
|
|
114
|
+
screen.subscribeSelector((state) => state.count, renderCount);
|
|
115
|
+
renderCount(bridge.getSnapshot().count); // Subscriptions do not emit initially.
|
|
116
|
+
screen.onCommand(async () => {
|
|
117
|
+
const count = await readCount();
|
|
118
|
+
screen.setSnapshot({ count });
|
|
119
|
+
screen.emit({ type: 'refreshed' });
|
|
120
|
+
});
|
|
121
|
+
screen.dispatch({ type: 'refresh' });
|
|
122
|
+
screen.dispose(); // A later readCount result cannot update this or a new screen.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`getSnapshot()` remains referentially stable until `setSnapshot` receives a value
|
|
126
|
+
that differs under `Object.is`. Snapshots and command/event payloads belong to the
|
|
127
|
+
consumer: publish immutable values and replace changed state. The bridge neither
|
|
128
|
+
deep-clones nor freezes arbitrary consumer or engine objects. Only bridge and
|
|
129
|
+
scope API containers are frozen; listener lists and pending deliveries stay
|
|
130
|
+
private. There is no frame-based copying, timer, or global singleton.
|
|
131
|
+
|
|
132
|
+
`subscribeSelector(selector, listener, equality = Object.is)` evaluates its
|
|
133
|
+
initial selection without notifying. Unchanged selected values suppress delivery.
|
|
134
|
+
Selectors and equality functions must be pure. Initial selector failures reject
|
|
135
|
+
registration; subsequent selector, equality, or listener failures are isolated
|
|
136
|
+
and reported through `onListenerError`. A failed selection leaves the previous
|
|
137
|
+
selection intact. An invoked listener receives the new selection even if another
|
|
138
|
+
listener changed current state reentrantly.
|
|
139
|
+
|
|
140
|
+
All notifications share one FIFO delivery queue. Each dispatch captures its
|
|
141
|
+
value and recipients. Reentrant mutations update current state immediately, but
|
|
142
|
+
their notifications run after the current round. Unsubscribed recipients are
|
|
143
|
+
skipped and new registrations wait for future dispatches. Duplicate registrations
|
|
144
|
+
are independent. As with the execution controller, synchronous exceptions and
|
|
145
|
+
rejected promises are observed without awaiting; errors from the optional error
|
|
146
|
+
hook are consumed. Observation failure never converts a business operation into
|
|
147
|
+
failure. Avoid self-sustaining dispatch cycles.
|
|
148
|
+
|
|
149
|
+
Scopes own subscriptions and cleanup via `own(cleanup)`. Its returned function
|
|
150
|
+
releases that resource once, and removes it from the scope's retained cleanup
|
|
151
|
+
set. Disposal first revokes callback and commit permission, then runs registered
|
|
152
|
+
cleanups in registration order. Cleanup errors do not prevent remaining cleanup.
|
|
153
|
+
An asynchronously acquired resource passed to `own` after disposal is immediately
|
|
154
|
+
released. This is the one deliberately supported late registration; ordinary new
|
|
155
|
+
subscriptions on a disposed scope throw.
|
|
156
|
+
|
|
157
|
+
After disposal, scoped `setSnapshot`, `emit`, and `dispatch` return `false`.
|
|
158
|
+
They cannot affect another screen scope. Raw bridge methods are application-owner
|
|
159
|
+
APIs; passing those directly to a screen's asynchronous work bypasses this guard.
|
|
160
|
+
Scope disposal does not cancel a request, server verification, or reward claim.
|
|
161
|
+
Such business operations need an owner whose lifetime exceeds the screen.
|
|
162
|
+
|
|
163
|
+
Bridge destruction disposes all scopes, removes listeners and queued deliveries,
|
|
164
|
+
and rejects new registration, dispatch, emit, or setSnapshot calls. The final
|
|
165
|
+
snapshot remains readable. Disposal, destruction, and unsubscribe are idempotent;
|
|
166
|
+
late scoped commits still return `false` after bridge destruction.
|
|
167
|
+
|
|
168
|
+
The UI subpath shares the private package's publication prerequisites. No Sampo
|
|
169
|
+
changeset or generated-game dependency is added for this private-only extension.
|
|
170
|
+
|
|
171
|
+
## Platform lifecycle binding
|
|
172
|
+
|
|
173
|
+
Import `bindGameLifecycle` from `@mpgd/game-runtime/platform`. Supply a controller,
|
|
174
|
+
a minimal `source` with `onPause`/`onResume` subscriptions (compatible with
|
|
175
|
+
`PlatformGateway.lifecycle`), and either an explicit `initialState` or a
|
|
176
|
+
`readState()` callback. States are `active`, `inactive`, and `unknown`; unknown
|
|
177
|
+
conservatively blocks. Default channels are all four execution channels.
|
|
178
|
+
|
|
179
|
+
Subscriptions install before reading current state. Events received during
|
|
180
|
+
installation override an explicit initial state; an event during `readState`
|
|
181
|
+
overrides that read's return value. A readable source should return its current
|
|
182
|
+
state synchronously. No DOM or SDK is imported and LifecycleAdapter is unchanged.
|
|
183
|
+
|
|
184
|
+
Each binding owns at most one token. Duplicate pause/resume events are idempotent;
|
|
185
|
+
a resume cannot release settings or another source's block. `dispose()` removes
|
|
186
|
+
subscriptions and releases only its own token. Source callbacks captured before
|
|
187
|
+
disposal become harmless, and controller destruction automatically detaches the
|
|
188
|
+
binding. Setup failures clean installed subscriptions; optional `onError` observes
|
|
189
|
+
cleanup errors without preventing remaining cleanup.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const lifecycleBinding = bindGameLifecycle({
|
|
193
|
+
controller: runtime,
|
|
194
|
+
source: gateway.lifecycle,
|
|
195
|
+
initialState: 'unknown',
|
|
196
|
+
});
|
|
197
|
+
// A later source resume can release this binding's conservative startup block.
|
|
198
|
+
lifecycleBinding.dispose();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Purchase and rewarded-ad actions
|
|
202
|
+
|
|
203
|
+
`@mpgd/game-runtime/actions` provides `createGameActionCoordinator`,
|
|
204
|
+
`createPurchaseActionController` and `createRewardedAdActionController`. Inject the
|
|
205
|
+
existing `GameServicesClient` (or its DOM-free `GameServicesOperationClient` port).
|
|
206
|
+
The controllers call only `purchase` and `claimRewardedAd`; they do not call an SDK,
|
|
207
|
+
verify a receipt, retry a transaction or grant local currency.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { createGameExecutionController } from '@mpgd/game-runtime';
|
|
211
|
+
import { createGameActionCoordinator } from '@mpgd/game-runtime/actions';
|
|
212
|
+
import { createGameUiBridge } from '@mpgd/game-runtime/ui';
|
|
213
|
+
|
|
214
|
+
// Application lifetime: one coordinator per runtime/client/player context.
|
|
215
|
+
const execution = createGameExecutionController();
|
|
216
|
+
const coordinator = createGameActionCoordinator({ execution, client });
|
|
217
|
+
const purchase = coordinator.createPurchaseController();
|
|
218
|
+
const ui = createGameUiBridge<string, never, string>({ initialSnapshot: 'idle' });
|
|
219
|
+
const screen = ui.createScope();
|
|
220
|
+
const view = purchase.bindScope(screen, {
|
|
221
|
+
snapshot: (value) => value.status,
|
|
222
|
+
event: (value) => `purchase:${value.status}`,
|
|
223
|
+
});
|
|
224
|
+
const result = view.execute({ productId: 'example', source: 'shop', idempotencyKey: suppliedKey });
|
|
225
|
+
screen.dispose(); // Detaches this screen; the service invocation and its block continue.
|
|
226
|
+
await result; // The owner also retains its safe completion snapshot through getSnapshot().
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```mermaid
|
|
230
|
+
flowchart TD
|
|
231
|
+
App[Application / player context] --> Coordinator[Shared execution coordinator]
|
|
232
|
+
Coordinator --> Purchase[Purchase operation owner]
|
|
233
|
+
Coordinator --> Ad[Rewarded-ad operation owner]
|
|
234
|
+
Purchase --> A[Screen A scope]
|
|
235
|
+
Purchase --> B[Screen B scope]
|
|
236
|
+
Coordinator --> Client[Existing GameServicesClient]
|
|
237
|
+
Client --> Ledger[Existing platform and backend ledger flow]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
An owner outlives its views. `subscribe` observes safe owner snapshots without an
|
|
241
|
+
initial delivery. `bindScope` projects only operations explicitly executed or joined
|
|
242
|
+
through that view; merely opening screen B never subscribes B to screen A's old
|
|
243
|
+
completion. Scope disposal removes UI subscriptions and commit authority. It does
|
|
244
|
+
not cancel the service promise. Owner `dispose` rejects new executions and removes
|
|
245
|
+
owner UI listeners, but preserves the eventual snapshot of an already started
|
|
246
|
+
operation. Coordinator disposal prevents new work across its owners. Neither
|
|
247
|
+
kind of disposal claims to roll back an external purchase.
|
|
248
|
+
|
|
249
|
+
| State | Meaning and handling |
|
|
250
|
+
| --- | --- |
|
|
251
|
+
| `idle` | No operation observed by this owner. |
|
|
252
|
+
| `running` | Local service call in flight; optional `progress` is a real service observation. |
|
|
253
|
+
| `granted` | Existing service result reports a grant. Read authoritative economy state through existing APIs. |
|
|
254
|
+
| `cancelled` (purchase), `skipped` (ad) | Preserve the service outcome. No automatic retry. |
|
|
255
|
+
| `pending` (purchase) | Unresolved transaction; local gameplay block ends, reconciliation is still required. |
|
|
256
|
+
| `unavailable` (ad) | Service cannot provide this ad. |
|
|
257
|
+
| `rejected` / `failed` | Preserve these distinct service outcomes; no local grant and no automatic retry. |
|
|
258
|
+
| `exception` | Call threw; final transaction result is unknown. Original error rejects the returned Promise, not the UI snapshot. |
|
|
259
|
+
|
|
260
|
+
Snapshots contain only kind, status, coordinator-local operation ID and whitelisted
|
|
261
|
+
progress fields. They never contain receipt/evidence, raw server bodies, ledger
|
|
262
|
+
objects, player identity or provider error text. Progress does not prove grant or
|
|
263
|
+
native UI visibility/closure. A legacy client that ignores options stays `running`
|
|
264
|
+
until its result settles. Listener and projection errors, including rejected async
|
|
265
|
+
listeners, are isolated through `onObserverError`.
|
|
266
|
+
|
|
267
|
+
Each local invocation owns a simulation/gameplay-input token from before the service
|
|
268
|
+
call until settlement. Settings/background tokens remain independent. No timer,
|
|
269
|
+
SDK-visibility guess, polling or `Promise.race` releases a block early. A `pending`
|
|
270
|
+
result releases this local block but is not considered a cancelled transaction.
|
|
271
|
+
|
|
272
|
+
The coordinator serializes purchases and ads for its injected client. Identical
|
|
273
|
+
in-flight keys/inputs share the exact Promise across recreated owners. A key reused
|
|
274
|
+
with a different kind, product, source or placement rejects with `key-conflict`.
|
|
275
|
+
Different in-flight work rejects with `busy`. The most recent completed operation
|
|
276
|
+
can be explicitly observed again through the retained Promise; earlier completed
|
|
277
|
+
keys reject with `already-completed` and are never re-invoked.
|
|
278
|
+
|
|
279
|
+
Only input fingerprints are remembered, at most `maxRememberedKeys` (default 1024,
|
|
280
|
+
allowed 1–10000). Keys are never evicted silently: reaching the bound rejects new
|
|
281
|
+
keys with `history-full`. The coordinator retains at most one completed result
|
|
282
|
+
Promise, not an unbounded result/event log. It is scoped to the current process;
|
|
283
|
+
server ledger idempotency remains authoritative. Creating another coordinator or
|
|
284
|
+
restarting the process is outside this guarantee. Do not recreate it per screen or
|
|
285
|
+
to bypass unresolved work, and do not generate a new key for each UI retry.
|
|
286
|
+
|
|
287
|
+
After `pending` or an invoked operation exception, new keys reject with
|
|
288
|
+
`reconciliation-required`. The current client has no recovery/requery port, so this
|
|
289
|
+
version deliberately provides no reset/retry/polling API. Integrate the existing
|
|
290
|
+
provider/backend recovery policy outside these UI actions before starting a new
|
|
291
|
+
application coordination session. Do not treat a retryable hint as permission to
|
|
292
|
+
repurchase. Input/preflight scheduling rejections occur before the service call and
|
|
293
|
+
do not invent a business outcome.
|
|
294
|
+
|
|
295
|
+
Packaging: `/actions` uses **type-only** imports from `@mpgd/game-services/operations`.
|
|
296
|
+
The workspace dependency ensures declarations/build order; neither the basic
|
|
297
|
+
runtime import nor actions import loads the service implementation, Phaser or DOM.
|
|
298
|
+
Consumers use the repository-standard `skipLibCheck` for third-party typia
|
|
299
|
+
ambient declarations; the headless consumer smoke supplies only ES2022 globals.
|
|
300
|
+
This package remains private. Future publication requires initial npm registration,
|
|
301
|
+
OIDC, and the game-services release containing `/operations` and progress options
|
|
302
|
+
(planned 0.15.0). No generated game gains a dependency on this unpublished package.
|
|
303
|
+
|
|
304
|
+
The owner that reserves an operation controls its pre-invocation startup permission.
|
|
305
|
+
A reentrant same-key joiner cannot cancel that owner's startup by disposing itself.
|
|
306
|
+
If owner/runtime disposal prevents any client invocation, the flight rejects with
|
|
307
|
+
a scheduling error, resets its observed state to `idle`, and emits no business
|
|
308
|
+
completion/exception event. An invoked client failure remains `exception`.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { GameServicesOperationClient, GameServicesPurchaseInput, GameServicesPurchaseProgress, GameServicesPurchaseResult, GameServicesRewardedAdInput, GameServicesRewardedAdProgress, GameServicesRewardedAdResult } from '@mpgd/game-services/operations';
|
|
2
|
+
import type { GameExecutionController } from '../index.js';
|
|
3
|
+
import { type ObserverErrorHandler } from '../observers.js';
|
|
4
|
+
import { type GameUiScope, type UiListener } from '../ui/index.js';
|
|
5
|
+
export type GameActionKind = 'purchase' | 'rewarded-ad';
|
|
6
|
+
interface Inputs {
|
|
7
|
+
purchase: GameServicesPurchaseInput;
|
|
8
|
+
'rewarded-ad': GameServicesRewardedAdInput;
|
|
9
|
+
}
|
|
10
|
+
interface Results {
|
|
11
|
+
purchase: GameServicesPurchaseResult;
|
|
12
|
+
'rewarded-ad': GameServicesRewardedAdResult;
|
|
13
|
+
}
|
|
14
|
+
interface Progress {
|
|
15
|
+
purchase: GameServicesPurchaseProgress;
|
|
16
|
+
'rewarded-ad': GameServicesRewardedAdProgress;
|
|
17
|
+
}
|
|
18
|
+
export type GameActionSnapshot<K extends GameActionKind> = Readonly<{
|
|
19
|
+
kind: K;
|
|
20
|
+
} & ({
|
|
21
|
+
status: 'idle';
|
|
22
|
+
} | {
|
|
23
|
+
status: 'running';
|
|
24
|
+
operationId: number;
|
|
25
|
+
progress?: Progress[K];
|
|
26
|
+
} | {
|
|
27
|
+
status: Results[K]['status'] | 'exception';
|
|
28
|
+
operationId: number;
|
|
29
|
+
})>;
|
|
30
|
+
export type PurchaseActionSnapshot = GameActionSnapshot<'purchase'>;
|
|
31
|
+
export type RewardedAdActionSnapshot = GameActionSnapshot<'rewarded-ad'>;
|
|
32
|
+
export type GameActionErrorCode = 'disposed' | 'busy' | 'key-conflict' | 'already-completed' | 'reconciliation-required' | 'history-full' | 'invalid-input';
|
|
33
|
+
/** Scheduling/preflight rejection, distinct from a service result or external exception. */
|
|
34
|
+
export declare class GameActionExecutionError extends Error {
|
|
35
|
+
readonly code: GameActionErrorCode;
|
|
36
|
+
constructor(code: GameActionErrorCode);
|
|
37
|
+
}
|
|
38
|
+
export interface GameActionView<K extends GameActionKind> {
|
|
39
|
+
execute(input: Inputs[K]): Promise<Results[K]>;
|
|
40
|
+
/** Detaches this view; it cannot cancel an already started service operation. */
|
|
41
|
+
dispose(): void;
|
|
42
|
+
}
|
|
43
|
+
export interface GameActionController<K extends GameActionKind> extends GameActionView<K> {
|
|
44
|
+
getSnapshot(): GameActionSnapshot<K>;
|
|
45
|
+
/** No initial delivery. Observer failures cannot change the service result. */
|
|
46
|
+
subscribe(listener: UiListener<GameActionSnapshot<K>>): () => void;
|
|
47
|
+
isDisposed(): boolean;
|
|
48
|
+
/** Only actions explicitly executed/joined through this view may update its scope. */
|
|
49
|
+
bindScope<S, C, E>(scope: GameUiScope<S, C, E>, projection: {
|
|
50
|
+
snapshot(value: GameActionSnapshot<K>): S;
|
|
51
|
+
event?(value: GameActionSnapshot<K>): E;
|
|
52
|
+
}): GameActionView<K>;
|
|
53
|
+
}
|
|
54
|
+
export interface GameActionCoordinator {
|
|
55
|
+
createPurchaseController(): GameActionController<'purchase'>;
|
|
56
|
+
createRewardedAdController(): GameActionController<'rewarded-ad'>;
|
|
57
|
+
getAvailability(): 'ready' | 'busy' | 'reconciliation-required' | 'history-full' | 'disposed';
|
|
58
|
+
/** Terminal for new calls; pending external work still settles and releases its own block. */
|
|
59
|
+
dispose(): void;
|
|
60
|
+
}
|
|
61
|
+
/** Bind one coordinator to one runtime/client (including its player identity), above all screens. */
|
|
62
|
+
export declare function createGameActionCoordinator(options: {
|
|
63
|
+
readonly execution: GameExecutionController;
|
|
64
|
+
readonly client: Pick<GameServicesOperationClient, 'purchase' | 'claimRewardedAd'>;
|
|
65
|
+
/** Never evicts keys: once full, new keys are rejected until application teardown. Default 1024. */
|
|
66
|
+
readonly maxRememberedKeys?: number;
|
|
67
|
+
readonly onObserverError?: ObserverErrorHandler;
|
|
68
|
+
}): GameActionCoordinator;
|
|
69
|
+
export declare function createPurchaseActionController(input: {
|
|
70
|
+
coordinator: GameActionCoordinator;
|
|
71
|
+
}): GameActionController<'purchase'>;
|
|
72
|
+
export declare function createRewardedAdActionController(input: {
|
|
73
|
+
coordinator: GameActionCoordinator;
|
|
74
|
+
}): GameActionController<'rewarded-ad'>;
|
|
75
|
+
export {};
|