@mmapp/react 0.1.0-alpha.1 → 0.1.0-alpha.4
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/README.md +112 -0
- package/dist/index.d.mts +1378 -94
- package/dist/index.d.ts +1378 -94
- package/dist/index.js +1094 -1309
- package/dist/index.mjs +1038 -1296
- package/package.json +4 -3
- package/package.json.backup +0 -41
- package/src/Blueprint.ts +0 -216
- package/src/__tests__/Blueprint.test.ts +0 -106
- package/src/__tests__/action-context.test.ts +0 -166
- package/src/__tests__/actionCreators.test.ts +0 -179
- package/src/__tests__/builders.test.ts +0 -336
- package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
- package/src/__tests__/factories.test.ts +0 -229
- package/src/__tests__/loader.test.ts +0 -159
- package/src/__tests__/logger.test.ts +0 -70
- package/src/__tests__/type-inference.test.ts +0 -160
- package/src/__tests__/typed-transitions.test.ts +0 -126
- package/src/__tests__/useModuleConfig.test.ts +0 -61
- package/src/actionCreators.ts +0 -132
- package/src/actions.ts +0 -547
- package/src/atoms/index.ts +0 -600
- package/src/authoring.ts +0 -92
- package/src/browser-player.ts +0 -783
- package/src/builders.ts +0 -1342
- package/src/components/ExperienceWorkflowBridge.tsx +0 -123
- package/src/components/PlayerProvider.tsx +0 -43
- package/src/components/atoms/index.tsx +0 -269
- package/src/components/index.ts +0 -36
- package/src/conditions.ts +0 -692
- package/src/config/defineBlueprint.ts +0 -329
- package/src/config/defineModel.ts +0 -753
- package/src/config/defineWorkspace.ts +0 -24
- package/src/core/WorkflowRuntime.ts +0 -153
- package/src/factories.ts +0 -425
- package/src/grammar/index.ts +0 -173
- package/src/hooks/index.ts +0 -106
- package/src/hooks/useAuth.ts +0 -288
- package/src/hooks/useChannel.ts +0 -304
- package/src/hooks/useComputed.ts +0 -154
- package/src/hooks/useDomainSubscription.ts +0 -110
- package/src/hooks/useDuringAction.ts +0 -99
- package/src/hooks/useExperienceState.ts +0 -59
- package/src/hooks/useExpressionLibrary.ts +0 -129
- package/src/hooks/useForm.ts +0 -352
- package/src/hooks/useGeolocation.ts +0 -207
- package/src/hooks/useMapView.ts +0 -259
- package/src/hooks/useMiddleware.ts +0 -291
- package/src/hooks/useModel.ts +0 -363
- package/src/hooks/useModule.ts +0 -59
- package/src/hooks/useModuleConfig.ts +0 -61
- package/src/hooks/useMutation.ts +0 -237
- package/src/hooks/useNotification.ts +0 -151
- package/src/hooks/useOnChange.ts +0 -30
- package/src/hooks/useOnEnter.ts +0 -59
- package/src/hooks/useOnEvent.ts +0 -37
- package/src/hooks/useOnExit.ts +0 -27
- package/src/hooks/useOnTransition.ts +0 -30
- package/src/hooks/usePackage.ts +0 -128
- package/src/hooks/useParams.ts +0 -33
- package/src/hooks/usePlayer.ts +0 -308
- package/src/hooks/useQuery.ts +0 -184
- package/src/hooks/useRealtimeQuery.ts +0 -222
- package/src/hooks/useRole.ts +0 -191
- package/src/hooks/useRouteParams.ts +0 -100
- package/src/hooks/useRouter.ts +0 -347
- package/src/hooks/useServerAction.ts +0 -178
- package/src/hooks/useServerState.ts +0 -284
- package/src/hooks/useToast.ts +0 -164
- package/src/hooks/useTransition.ts +0 -39
- package/src/hooks/useView.ts +0 -102
- package/src/hooks/useWhileIn.ts +0 -48
- package/src/hooks/useWorkflow.ts +0 -63
- package/src/index.ts +0 -465
- package/src/loader/experience-workflow-loader.ts +0 -192
- package/src/loader/index.ts +0 -6
- package/src/local/LocalEngine.ts +0 -388
- package/src/local/LocalEngineAdapter.ts +0 -175
- package/src/local/LocalEngineContext.ts +0 -30
- package/src/logger.ts +0 -37
- package/src/mixins.ts +0 -1160
- package/src/providers/RuntimeContext.ts +0 -20
- package/src/providers/WorkflowProvider.tsx +0 -28
- package/src/routing/instance-key.ts +0 -107
- package/src/server/transition-context.ts +0 -172
- package/src/testing/index.ts +0 -9
- package/src/testing/useBlueprintTestRunner.ts +0 -91
- package/src/testing/useGraphAnalysis.ts +0 -18
- package/src/testing/useTestRunner.ts +0 -77
- package/src/testing.ts +0 -995
- package/src/types/workflow-inference.ts +0 -158
- package/src/types.ts +0 -114
- package/tsconfig.json +0 -27
- package/vitest.config.ts +0 -8
package/src/browser-player.ts
DELETED
|
@@ -1,783 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BrowserPlayer — Unified browser-side MindMatrix P2P player.
|
|
3
|
-
*
|
|
4
|
-
* Wires together:
|
|
5
|
-
* - mm-wasm (expression engine, transition execution, Ed25519 crypto)
|
|
6
|
-
* - mm-storage-browser (IndexedDB persistence via WASM)
|
|
7
|
-
* - WebSocket signaling (for P2P peer discovery via mm-bridge)
|
|
8
|
-
*
|
|
9
|
-
* Provides a framework-agnostic API: init(), createInstance(), transition(),
|
|
10
|
-
* subscribe(), getState(). React hooks (usePlayer, etc.) can wrap this.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* const player = new BrowserPlayer({
|
|
15
|
-
* signalingUrl: 'wss://bridge.mindmatrix.io/ws/signal',
|
|
16
|
-
* wasmUrl: '/mm_wasm_bg.wasm',
|
|
17
|
-
* });
|
|
18
|
-
* await player.init();
|
|
19
|
-
* const instance = await player.createInstance('my-workflow', { title: 'Hello' });
|
|
20
|
-
* await player.transition(instance.id, 'submit', { approved: true });
|
|
21
|
-
* player.subscribe('state:*', (event) => console.log(event));
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
/** WASM module interface (mm-wasm exports) */
|
|
28
|
-
export interface MmWasmModule {
|
|
29
|
-
evaluate_expression(expr: string, contextJson: string, evalContext?: string): string;
|
|
30
|
-
evaluate_batch(expressionsJson: string, contextJson: string, evalContext?: string): string;
|
|
31
|
-
execute_transition?(requestJson: string): string;
|
|
32
|
-
generate_keypair(): string;
|
|
33
|
-
get_peer_id(secretKeyHex: string): string;
|
|
34
|
-
sign_data(data: Uint8Array, secretKeyHex: string): string;
|
|
35
|
-
sign_envelope(secretKeyHex: string, payloadJson: string): string;
|
|
36
|
-
verify_envelope_signature(envelopeJson: string): boolean;
|
|
37
|
-
encrypt_topic_payload(keyHex: string, plaintext: Uint8Array): string;
|
|
38
|
-
decrypt_topic_payload(keyHex: string, ciphertextHex: string): Uint8Array;
|
|
39
|
-
version(): string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Configuration for BrowserPlayer */
|
|
43
|
-
export interface BrowserPlayerConfig {
|
|
44
|
-
/** URL to the mm-wasm .wasm file */
|
|
45
|
-
wasmUrl?: string;
|
|
46
|
-
/** Pre-loaded WASM module (skip loading from URL) */
|
|
47
|
-
wasmModule?: MmWasmModule;
|
|
48
|
-
/** WebSocket signaling URL (mm-bridge /ws/signal endpoint) */
|
|
49
|
-
signalingUrl?: string;
|
|
50
|
-
/** JWT token for signaling authentication */
|
|
51
|
-
authToken?: string;
|
|
52
|
-
/** IndexedDB database name (default: "mm-player") */
|
|
53
|
-
dbName?: string;
|
|
54
|
-
/** Identity label for IndexedDB key storage */
|
|
55
|
-
identityLabel?: string;
|
|
56
|
-
/** Pre-existing secret key hex (skip key generation/loading) */
|
|
57
|
-
secretKeyHex?: string;
|
|
58
|
-
/** Enable debug logging */
|
|
59
|
-
debug?: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** A workflow instance managed by the player */
|
|
63
|
-
export interface PlayerInstance {
|
|
64
|
-
id: string;
|
|
65
|
-
definitionId: string;
|
|
66
|
-
definitionSlug: string;
|
|
67
|
-
currentState: string;
|
|
68
|
-
status: 'ACTIVE' | 'COMPLETED' | 'CANCELLED';
|
|
69
|
-
stateData: Record<string, unknown>;
|
|
70
|
-
memory: Record<string, unknown>;
|
|
71
|
-
lockVersion: number;
|
|
72
|
-
createdAt: string;
|
|
73
|
-
updatedAt: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Result of a transition execution */
|
|
77
|
-
export interface TransitionResult {
|
|
78
|
-
success: boolean;
|
|
79
|
-
instance: PlayerInstance;
|
|
80
|
-
error?: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Event emitted by the player */
|
|
84
|
-
export interface PlayerEvent {
|
|
85
|
-
type: string;
|
|
86
|
-
instanceId?: string;
|
|
87
|
-
data: Record<string, unknown>;
|
|
88
|
-
timestamp: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Subscription callback */
|
|
92
|
-
export type PlayerEventCallback = (event: PlayerEvent) => void;
|
|
93
|
-
|
|
94
|
-
/** Subscription handle (for unsubscribing) */
|
|
95
|
-
export interface Subscription {
|
|
96
|
-
id: string;
|
|
97
|
-
unsubscribe: () => void;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Signaling message from server */
|
|
101
|
-
interface SignalingMessage {
|
|
102
|
-
event: string;
|
|
103
|
-
data: Record<string, unknown>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ─── Identity Store (IndexedDB) ─────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
const IDENTITY_DB_NAME = 'mm-identity';
|
|
109
|
-
const IDENTITY_STORE_NAME = 'keys';
|
|
110
|
-
|
|
111
|
-
interface StoredIdentity {
|
|
112
|
-
label: string;
|
|
113
|
-
secretKeyHex: string;
|
|
114
|
-
publicKeyHex: string;
|
|
115
|
-
createdAt: string;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function openIdentityDb(): Promise<IDBDatabase> {
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
const request = indexedDB.open(IDENTITY_DB_NAME, 1);
|
|
121
|
-
request.onupgradeneeded = () => {
|
|
122
|
-
const db = request.result;
|
|
123
|
-
if (!db.objectStoreNames.contains(IDENTITY_STORE_NAME)) {
|
|
124
|
-
db.createObjectStore(IDENTITY_STORE_NAME, { keyPath: 'label' });
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
request.onsuccess = () => resolve(request.result);
|
|
128
|
-
request.onerror = () => reject(request.error);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function loadIdentity(label: string): Promise<StoredIdentity | null> {
|
|
133
|
-
const db = await openIdentityDb();
|
|
134
|
-
return new Promise((resolve, reject) => {
|
|
135
|
-
const tx = db.transaction(IDENTITY_STORE_NAME, 'readonly');
|
|
136
|
-
const store = tx.objectStore(IDENTITY_STORE_NAME);
|
|
137
|
-
const request = store.get(label);
|
|
138
|
-
request.onsuccess = () => resolve(request.result ?? null);
|
|
139
|
-
request.onerror = () => reject(request.error);
|
|
140
|
-
tx.oncomplete = () => db.close();
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function saveIdentity(identity: StoredIdentity): Promise<void> {
|
|
145
|
-
const db = await openIdentityDb();
|
|
146
|
-
return new Promise((resolve, reject) => {
|
|
147
|
-
const tx = db.transaction(IDENTITY_STORE_NAME, 'readwrite');
|
|
148
|
-
const store = tx.objectStore(IDENTITY_STORE_NAME);
|
|
149
|
-
store.put(identity);
|
|
150
|
-
tx.oncomplete = () => {
|
|
151
|
-
db.close();
|
|
152
|
-
resolve();
|
|
153
|
-
};
|
|
154
|
-
tx.onerror = () => reject(tx.error);
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ─── Instance Store (IndexedDB) ─────────────────────────────────────────────
|
|
159
|
-
|
|
160
|
-
const INSTANCE_STORE_NAME = 'instances';
|
|
161
|
-
const DEFINITION_STORE_NAME = 'definitions';
|
|
162
|
-
|
|
163
|
-
async function openPlayerDb(dbName: string): Promise<IDBDatabase> {
|
|
164
|
-
return new Promise((resolve, reject) => {
|
|
165
|
-
const request = indexedDB.open(dbName, 1);
|
|
166
|
-
request.onupgradeneeded = () => {
|
|
167
|
-
const db = request.result;
|
|
168
|
-
if (!db.objectStoreNames.contains(INSTANCE_STORE_NAME)) {
|
|
169
|
-
db.createObjectStore(INSTANCE_STORE_NAME, { keyPath: 'id' });
|
|
170
|
-
}
|
|
171
|
-
if (!db.objectStoreNames.contains(DEFINITION_STORE_NAME)) {
|
|
172
|
-
db.createObjectStore(DEFINITION_STORE_NAME, { keyPath: 'id' });
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
request.onsuccess = () => resolve(request.result);
|
|
176
|
-
request.onerror = () => reject(request.error);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ─── BrowserPlayer ──────────────────────────────────────────────────────────
|
|
181
|
-
|
|
182
|
-
export class BrowserPlayer {
|
|
183
|
-
private config: Required<
|
|
184
|
-
Pick<BrowserPlayerConfig, 'dbName' | 'identityLabel' | 'debug'>
|
|
185
|
-
> &
|
|
186
|
-
BrowserPlayerConfig;
|
|
187
|
-
private wasm: MmWasmModule | null = null;
|
|
188
|
-
private secretKeyHex: string | null = null;
|
|
189
|
-
private publicKeyHex: string | null = null;
|
|
190
|
-
private peerId: string | null = null;
|
|
191
|
-
private db: IDBDatabase | null = null;
|
|
192
|
-
private ws: WebSocket | null = null;
|
|
193
|
-
private subscriptions = new Map<string, Set<PlayerEventCallback>>();
|
|
194
|
-
private initialized = false;
|
|
195
|
-
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
196
|
-
|
|
197
|
-
constructor(config: BrowserPlayerConfig = {}) {
|
|
198
|
-
this.config = {
|
|
199
|
-
...config,
|
|
200
|
-
dbName: config.dbName ?? 'mm-player',
|
|
201
|
-
identityLabel: config.identityLabel ?? 'default',
|
|
202
|
-
debug: config.debug ?? false,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ─── Lifecycle ──────────────────────────────────────────────────────
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Initialize the player:
|
|
210
|
-
* 1. Load or generate Ed25519 identity (IndexedDB)
|
|
211
|
-
* 2. Initialize WASM module
|
|
212
|
-
* 3. Open instance store (IndexedDB)
|
|
213
|
-
* 4. Connect to signaling server (if configured)
|
|
214
|
-
*/
|
|
215
|
-
async init(): Promise<void> {
|
|
216
|
-
if (this.initialized) return;
|
|
217
|
-
|
|
218
|
-
// 1. WASM module
|
|
219
|
-
if (this.config.wasmModule) {
|
|
220
|
-
this.wasm = this.config.wasmModule;
|
|
221
|
-
} else if (this.config.wasmUrl) {
|
|
222
|
-
this.wasm = await this.loadWasm(this.config.wasmUrl);
|
|
223
|
-
} else {
|
|
224
|
-
throw new Error('BrowserPlayer: either wasmUrl or wasmModule is required');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
this.log('info', `WASM module loaded (version: ${this.wasm.version()})`);
|
|
228
|
-
|
|
229
|
-
// 2. Identity
|
|
230
|
-
if (this.config.secretKeyHex) {
|
|
231
|
-
this.secretKeyHex = this.config.secretKeyHex;
|
|
232
|
-
this.publicKeyHex = this.wasm.get_peer_id(this.secretKeyHex);
|
|
233
|
-
} else {
|
|
234
|
-
await this.loadOrCreateIdentity();
|
|
235
|
-
}
|
|
236
|
-
this.peerId = this.publicKeyHex;
|
|
237
|
-
this.log('info', `Identity ready: ${this.peerId?.slice(0, 16)}...`);
|
|
238
|
-
|
|
239
|
-
// 3. Instance store
|
|
240
|
-
this.db = await openPlayerDb(this.config.dbName);
|
|
241
|
-
this.log('info', `IndexedDB "${this.config.dbName}" opened`);
|
|
242
|
-
|
|
243
|
-
// 4. Signaling
|
|
244
|
-
if (this.config.signalingUrl) {
|
|
245
|
-
this.connectSignaling();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
this.initialized = true;
|
|
249
|
-
this.emit({ type: 'player:ready', data: { peerId: this.peerId }, timestamp: Date.now() });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Shut down the player, close connections. */
|
|
253
|
-
async destroy(): Promise<void> {
|
|
254
|
-
if (this.reconnectTimer) {
|
|
255
|
-
clearTimeout(this.reconnectTimer);
|
|
256
|
-
this.reconnectTimer = null;
|
|
257
|
-
}
|
|
258
|
-
if (this.ws) {
|
|
259
|
-
this.ws.close();
|
|
260
|
-
this.ws = null;
|
|
261
|
-
}
|
|
262
|
-
if (this.db) {
|
|
263
|
-
this.db.close();
|
|
264
|
-
this.db = null;
|
|
265
|
-
}
|
|
266
|
-
this.subscriptions.clear();
|
|
267
|
-
this.initialized = false;
|
|
268
|
-
this.log('info', 'BrowserPlayer destroyed');
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ─── Identity ───────────────────────────────────────────────────────
|
|
272
|
-
|
|
273
|
-
/** Get the local peer ID (hex-encoded Ed25519 public key). */
|
|
274
|
-
getPeerId(): string {
|
|
275
|
-
this.ensureInitialized();
|
|
276
|
-
return this.peerId!;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Sign arbitrary data with the local identity. Returns hex-encoded signature. */
|
|
280
|
-
sign(data: Uint8Array): string {
|
|
281
|
-
this.ensureInitialized();
|
|
282
|
-
return this.wasm!.sign_data(data, this.secretKeyHex!);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/** Sign an envelope (MessagePayload JSON → signed Envelope JSON). */
|
|
286
|
-
signEnvelope(payloadJson: string): string {
|
|
287
|
-
this.ensureInitialized();
|
|
288
|
-
return this.wasm!.sign_envelope(this.secretKeyHex!, payloadJson);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** Verify an envelope's signature. */
|
|
292
|
-
verifyEnvelope(envelopeJson: string): boolean {
|
|
293
|
-
this.ensureInitialized();
|
|
294
|
-
return this.wasm!.verify_envelope_signature(envelopeJson);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// ─── Expressions ────────────────────────────────────────────────────
|
|
298
|
-
|
|
299
|
-
/** Evaluate a single expression against a context. */
|
|
300
|
-
evaluateExpression(
|
|
301
|
-
expr: string,
|
|
302
|
-
context: Record<string, unknown>,
|
|
303
|
-
evalContext?: string,
|
|
304
|
-
): unknown {
|
|
305
|
-
this.ensureInitialized();
|
|
306
|
-
const result = this.wasm!.evaluate_expression(
|
|
307
|
-
expr,
|
|
308
|
-
JSON.stringify(context),
|
|
309
|
-
evalContext,
|
|
310
|
-
);
|
|
311
|
-
return JSON.parse(result);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Evaluate multiple expressions in a single call. */
|
|
315
|
-
evaluateBatch(
|
|
316
|
-
expressions: string[],
|
|
317
|
-
context: Record<string, unknown>,
|
|
318
|
-
evalContext?: string,
|
|
319
|
-
): unknown[] {
|
|
320
|
-
this.ensureInitialized();
|
|
321
|
-
const result = this.wasm!.evaluate_batch(
|
|
322
|
-
JSON.stringify(expressions),
|
|
323
|
-
JSON.stringify(context),
|
|
324
|
-
evalContext,
|
|
325
|
-
);
|
|
326
|
-
return JSON.parse(result);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ─── Instance Management ────────────────────────────────────────────
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Create a new workflow instance.
|
|
333
|
-
*
|
|
334
|
-
* @param definitionSlug - The definition slug to instantiate
|
|
335
|
-
* @param initialData - Initial state_data
|
|
336
|
-
* @param options - Optional metadata (entityType, entityId)
|
|
337
|
-
*/
|
|
338
|
-
async createInstance(
|
|
339
|
-
definitionSlug: string,
|
|
340
|
-
initialData: Record<string, unknown> = {},
|
|
341
|
-
options: {
|
|
342
|
-
definitionId?: string;
|
|
343
|
-
entityType?: string;
|
|
344
|
-
entityId?: string;
|
|
345
|
-
} = {},
|
|
346
|
-
): Promise<PlayerInstance> {
|
|
347
|
-
this.ensureInitialized();
|
|
348
|
-
|
|
349
|
-
const id = crypto.randomUUID();
|
|
350
|
-
const now = new Date().toISOString();
|
|
351
|
-
|
|
352
|
-
const instance: PlayerInstance = {
|
|
353
|
-
id,
|
|
354
|
-
definitionId: options.definitionId ?? definitionSlug,
|
|
355
|
-
definitionSlug,
|
|
356
|
-
currentState: 'START',
|
|
357
|
-
status: 'ACTIVE',
|
|
358
|
-
stateData: { ...initialData },
|
|
359
|
-
memory: {},
|
|
360
|
-
lockVersion: 0,
|
|
361
|
-
createdAt: now,
|
|
362
|
-
updatedAt: now,
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
await this.putInstance(instance);
|
|
366
|
-
this.log('info', `Instance created: ${id} (${definitionSlug})`);
|
|
367
|
-
|
|
368
|
-
this.emit({
|
|
369
|
-
type: 'instance:created',
|
|
370
|
-
instanceId: id,
|
|
371
|
-
data: { definitionSlug, currentState: instance.currentState },
|
|
372
|
-
timestamp: Date.now(),
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
return instance;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/** Get an instance by ID. */
|
|
379
|
-
async getInstance(id: string): Promise<PlayerInstance | null> {
|
|
380
|
-
this.ensureInitialized();
|
|
381
|
-
return this.getInstanceFromDb(id);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/** Get the current state of an instance. */
|
|
385
|
-
async getState(id: string): Promise<{
|
|
386
|
-
currentState: string;
|
|
387
|
-
status: string;
|
|
388
|
-
stateData: Record<string, unknown>;
|
|
389
|
-
lockVersion: number;
|
|
390
|
-
} | null> {
|
|
391
|
-
const instance = await this.getInstance(id);
|
|
392
|
-
if (!instance) return null;
|
|
393
|
-
return {
|
|
394
|
-
currentState: instance.currentState,
|
|
395
|
-
status: instance.status,
|
|
396
|
-
stateData: instance.stateData,
|
|
397
|
-
lockVersion: instance.lockVersion,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Execute a transition on an instance.
|
|
403
|
-
*
|
|
404
|
-
* If the WASM module has `execute_transition` (full build), uses the
|
|
405
|
-
* 13-step pipeline. Otherwise, performs a simple state update.
|
|
406
|
-
*/
|
|
407
|
-
async transition(
|
|
408
|
-
instanceId: string,
|
|
409
|
-
transitionName: string,
|
|
410
|
-
data: Record<string, unknown> = {},
|
|
411
|
-
): Promise<TransitionResult> {
|
|
412
|
-
this.ensureInitialized();
|
|
413
|
-
|
|
414
|
-
const instance = await this.getInstanceFromDb(instanceId);
|
|
415
|
-
if (!instance) {
|
|
416
|
-
return {
|
|
417
|
-
success: false,
|
|
418
|
-
instance: null as unknown as PlayerInstance,
|
|
419
|
-
error: `Instance ${instanceId} not found`,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (instance.status !== 'ACTIVE') {
|
|
424
|
-
return {
|
|
425
|
-
success: false,
|
|
426
|
-
instance,
|
|
427
|
-
error: `Instance is ${instance.status}, cannot transition`,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
try {
|
|
432
|
-
// Merge transition data into state_data.
|
|
433
|
-
const newStateData = { ...instance.stateData, ...data };
|
|
434
|
-
const newLockVersion = instance.lockVersion + 1;
|
|
435
|
-
|
|
436
|
-
// If WASM full build available, use the pipeline.
|
|
437
|
-
if (this.wasm!.execute_transition) {
|
|
438
|
-
const request = {
|
|
439
|
-
instance: {
|
|
440
|
-
id: instance.id,
|
|
441
|
-
definition_id: instance.definitionId,
|
|
442
|
-
current_state: instance.currentState,
|
|
443
|
-
status: instance.status,
|
|
444
|
-
state_data: newStateData,
|
|
445
|
-
memory: instance.memory,
|
|
446
|
-
execution_lock_version: instance.lockVersion,
|
|
447
|
-
},
|
|
448
|
-
transition_name: transitionName,
|
|
449
|
-
actor_id: this.peerId,
|
|
450
|
-
transition_data: data,
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
const resultJson = this.wasm!.execute_transition(JSON.stringify(request));
|
|
454
|
-
const result = JSON.parse(resultJson);
|
|
455
|
-
|
|
456
|
-
if (result.success === false || result.error) {
|
|
457
|
-
return { success: false, instance, error: result.error ?? 'Transition failed' };
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Apply pipeline result.
|
|
461
|
-
instance.currentState = result.to_state ?? instance.currentState;
|
|
462
|
-
instance.status = result.status ?? instance.status;
|
|
463
|
-
instance.stateData = result.state_data ?? newStateData;
|
|
464
|
-
instance.memory = result.memory ?? instance.memory;
|
|
465
|
-
} else {
|
|
466
|
-
// Simple state update (minimal WASM build without pipeline).
|
|
467
|
-
instance.stateData = newStateData;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
instance.lockVersion = newLockVersion;
|
|
471
|
-
instance.updatedAt = new Date().toISOString();
|
|
472
|
-
|
|
473
|
-
await this.putInstance(instance);
|
|
474
|
-
|
|
475
|
-
this.log('info', `Transition: ${instanceId} ${transitionName}`);
|
|
476
|
-
this.emit({
|
|
477
|
-
type: 'instance:transition',
|
|
478
|
-
instanceId,
|
|
479
|
-
data: {
|
|
480
|
-
transitionName,
|
|
481
|
-
fromState: instance.currentState,
|
|
482
|
-
toState: instance.currentState,
|
|
483
|
-
lockVersion: newLockVersion,
|
|
484
|
-
},
|
|
485
|
-
timestamp: Date.now(),
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
// Sign and broadcast if connected to signaling.
|
|
489
|
-
if (this.ws?.readyState === WebSocket.OPEN && this.secretKeyHex) {
|
|
490
|
-
const payload = JSON.stringify({
|
|
491
|
-
type: 'TransitionReplication',
|
|
492
|
-
instance_id: instanceId,
|
|
493
|
-
transition_name: transitionName,
|
|
494
|
-
state_data: instance.stateData,
|
|
495
|
-
lock_version: newLockVersion,
|
|
496
|
-
});
|
|
497
|
-
const envelope = this.wasm!.sign_envelope(this.secretKeyHex, payload);
|
|
498
|
-
this.ws.send(JSON.stringify({
|
|
499
|
-
type: 'signal:broadcast',
|
|
500
|
-
topic: `mm/replication`,
|
|
501
|
-
envelope,
|
|
502
|
-
}));
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return { success: true, instance };
|
|
506
|
-
} catch (err) {
|
|
507
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
508
|
-
this.log('error', `Transition failed: ${error}`);
|
|
509
|
-
return { success: false, instance, error };
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// ─── Subscriptions ──────────────────────────────────────────────────
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Subscribe to player events.
|
|
517
|
-
*
|
|
518
|
-
* @param pattern - Event type pattern (supports `*` wildcard).
|
|
519
|
-
* Examples: `"instance:*"`, `"player:ready"`, `"peer:*"`, `"*"`
|
|
520
|
-
* @param callback - Called when a matching event occurs
|
|
521
|
-
* @returns Subscription handle with `unsubscribe()` method
|
|
522
|
-
*/
|
|
523
|
-
subscribe(pattern: string, callback: PlayerEventCallback): Subscription {
|
|
524
|
-
if (!this.subscriptions.has(pattern)) {
|
|
525
|
-
this.subscriptions.set(pattern, new Set());
|
|
526
|
-
}
|
|
527
|
-
this.subscriptions.get(pattern)!.add(callback);
|
|
528
|
-
|
|
529
|
-
const id = crypto.randomUUID();
|
|
530
|
-
return {
|
|
531
|
-
id,
|
|
532
|
-
unsubscribe: () => {
|
|
533
|
-
this.subscriptions.get(pattern)?.delete(callback);
|
|
534
|
-
},
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// ─── Topic Encryption ───────────────────────────────────────────────
|
|
539
|
-
|
|
540
|
-
/** Encrypt a payload for a topic (AES-256-GCM). */
|
|
541
|
-
encryptForTopic(topicKeyHex: string, plaintext: Uint8Array): string {
|
|
542
|
-
this.ensureInitialized();
|
|
543
|
-
return this.wasm!.encrypt_topic_payload(topicKeyHex, plaintext);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/** Decrypt a topic payload. */
|
|
547
|
-
decryptFromTopic(topicKeyHex: string, ciphertextHex: string): Uint8Array {
|
|
548
|
-
this.ensureInitialized();
|
|
549
|
-
return this.wasm!.decrypt_topic_payload(topicKeyHex, ciphertextHex);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// ─── Internal: WASM Loading ─────────────────────────────────────────
|
|
553
|
-
|
|
554
|
-
private async loadWasm(url: string): Promise<MmWasmModule> {
|
|
555
|
-
// Dynamic import of the wasm-bindgen generated JS glue.
|
|
556
|
-
// The URL should point to the wasm-pack output directory.
|
|
557
|
-
const module = await import(/* @vite-ignore */ url);
|
|
558
|
-
if (module.default && typeof module.default === 'function') {
|
|
559
|
-
// wasm-pack style: default export is init function
|
|
560
|
-
await module.default();
|
|
561
|
-
}
|
|
562
|
-
return module as MmWasmModule;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// ─── Internal: Identity ─────────────────────────────────────────────
|
|
566
|
-
|
|
567
|
-
private async loadOrCreateIdentity(): Promise<void> {
|
|
568
|
-
const label = this.config.identityLabel;
|
|
569
|
-
|
|
570
|
-
// Try loading from IndexedDB.
|
|
571
|
-
const stored = await loadIdentity(label);
|
|
572
|
-
if (stored) {
|
|
573
|
-
this.secretKeyHex = stored.secretKeyHex;
|
|
574
|
-
this.publicKeyHex = stored.publicKeyHex;
|
|
575
|
-
this.log('info', `Identity loaded from IndexedDB (label: ${label})`);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Generate new identity via WASM.
|
|
580
|
-
const keypairJson = this.wasm!.generate_keypair();
|
|
581
|
-
const keypair: { secret_key: string; public_key: string } = JSON.parse(keypairJson);
|
|
582
|
-
|
|
583
|
-
this.secretKeyHex = keypair.secret_key;
|
|
584
|
-
this.publicKeyHex = keypair.public_key;
|
|
585
|
-
|
|
586
|
-
// Persist to IndexedDB.
|
|
587
|
-
await saveIdentity({
|
|
588
|
-
label,
|
|
589
|
-
secretKeyHex: keypair.secret_key,
|
|
590
|
-
publicKeyHex: keypair.public_key,
|
|
591
|
-
createdAt: new Date().toISOString(),
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
this.log('info', `New identity generated and saved (label: ${label})`);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// ─── Internal: IndexedDB Instance Store ─────────────────────────────
|
|
598
|
-
|
|
599
|
-
private async getInstanceFromDb(id: string): Promise<PlayerInstance | null> {
|
|
600
|
-
if (!this.db) return null;
|
|
601
|
-
return new Promise((resolve, reject) => {
|
|
602
|
-
const tx = this.db!.transaction(INSTANCE_STORE_NAME, 'readonly');
|
|
603
|
-
const store = tx.objectStore(INSTANCE_STORE_NAME);
|
|
604
|
-
const request = store.get(id);
|
|
605
|
-
request.onsuccess = () => resolve(request.result ?? null);
|
|
606
|
-
request.onerror = () => reject(request.error);
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private async putInstance(instance: PlayerInstance): Promise<void> {
|
|
611
|
-
if (!this.db) return;
|
|
612
|
-
return new Promise((resolve, reject) => {
|
|
613
|
-
const tx = this.db!.transaction(INSTANCE_STORE_NAME, 'readwrite');
|
|
614
|
-
const store = tx.objectStore(INSTANCE_STORE_NAME);
|
|
615
|
-
store.put(instance);
|
|
616
|
-
tx.oncomplete = () => resolve();
|
|
617
|
-
tx.onerror = () => reject(tx.error);
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// ─── Internal: Signaling ────────────────────────────────────────────
|
|
622
|
-
|
|
623
|
-
private connectSignaling(): void {
|
|
624
|
-
if (!this.config.signalingUrl) return;
|
|
625
|
-
|
|
626
|
-
try {
|
|
627
|
-
this.ws = new WebSocket(this.config.signalingUrl);
|
|
628
|
-
} catch (err) {
|
|
629
|
-
this.log('error', `Signaling connection failed: ${err}`);
|
|
630
|
-
this.scheduleReconnect();
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
this.ws.onopen = () => {
|
|
635
|
-
this.log('info', 'Signaling connected');
|
|
636
|
-
|
|
637
|
-
// Authenticate.
|
|
638
|
-
if (this.config.authToken) {
|
|
639
|
-
this.ws!.send(
|
|
640
|
-
JSON.stringify({
|
|
641
|
-
type: 'signal:auth',
|
|
642
|
-
token: this.config.authToken,
|
|
643
|
-
}),
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
this.emit({
|
|
648
|
-
type: 'signaling:connected',
|
|
649
|
-
data: { url: this.config.signalingUrl! },
|
|
650
|
-
timestamp: Date.now(),
|
|
651
|
-
});
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
this.ws.onmessage = (event) => {
|
|
655
|
-
try {
|
|
656
|
-
const msg: SignalingMessage = JSON.parse(event.data as string);
|
|
657
|
-
this.handleSignalingMessage(msg);
|
|
658
|
-
} catch {
|
|
659
|
-
this.log('error', `Invalid signaling message: ${event.data}`);
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
this.ws.onclose = () => {
|
|
664
|
-
this.log('info', 'Signaling disconnected');
|
|
665
|
-
this.emit({
|
|
666
|
-
type: 'signaling:disconnected',
|
|
667
|
-
data: {},
|
|
668
|
-
timestamp: Date.now(),
|
|
669
|
-
});
|
|
670
|
-
this.scheduleReconnect();
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
this.ws.onerror = () => {
|
|
674
|
-
this.log('error', 'Signaling WebSocket error');
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
private handleSignalingMessage(msg: SignalingMessage): void {
|
|
679
|
-
switch (msg.event) {
|
|
680
|
-
case 'signal:auth':
|
|
681
|
-
this.log('info', `Authenticated as peer: ${msg.data.peer_id}`);
|
|
682
|
-
this.emit({
|
|
683
|
-
type: 'signaling:authenticated',
|
|
684
|
-
data: msg.data,
|
|
685
|
-
timestamp: Date.now(),
|
|
686
|
-
});
|
|
687
|
-
break;
|
|
688
|
-
|
|
689
|
-
case 'signal:peer-list':
|
|
690
|
-
this.emit({
|
|
691
|
-
type: 'peer:list',
|
|
692
|
-
data: msg.data,
|
|
693
|
-
timestamp: Date.now(),
|
|
694
|
-
});
|
|
695
|
-
break;
|
|
696
|
-
|
|
697
|
-
case 'signal:offer':
|
|
698
|
-
case 'signal:answer':
|
|
699
|
-
case 'signal:ice-candidate':
|
|
700
|
-
this.emit({
|
|
701
|
-
type: `signaling:${msg.event.replace('signal:', '')}`,
|
|
702
|
-
data: msg.data,
|
|
703
|
-
timestamp: Date.now(),
|
|
704
|
-
});
|
|
705
|
-
break;
|
|
706
|
-
|
|
707
|
-
case 'signal:turn-credentials':
|
|
708
|
-
this.emit({
|
|
709
|
-
type: 'signaling:turn-credentials',
|
|
710
|
-
data: msg.data,
|
|
711
|
-
timestamp: Date.now(),
|
|
712
|
-
});
|
|
713
|
-
break;
|
|
714
|
-
|
|
715
|
-
default:
|
|
716
|
-
this.log('debug', `Unknown signaling event: ${msg.event}`);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
private scheduleReconnect(): void {
|
|
721
|
-
if (this.reconnectTimer) return;
|
|
722
|
-
this.reconnectTimer = setTimeout(() => {
|
|
723
|
-
this.reconnectTimer = null;
|
|
724
|
-
if (this.config.signalingUrl && this.initialized) {
|
|
725
|
-
this.log('info', 'Attempting signaling reconnect...');
|
|
726
|
-
this.connectSignaling();
|
|
727
|
-
}
|
|
728
|
-
}, 5000);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// ─── Internal: Event System ─────────────────────────────────────────
|
|
732
|
-
|
|
733
|
-
private emit(event: PlayerEvent): void {
|
|
734
|
-
for (const [pattern, callbacks] of this.subscriptions) {
|
|
735
|
-
if (this.matchesPattern(pattern, event.type)) {
|
|
736
|
-
for (const cb of callbacks) {
|
|
737
|
-
try {
|
|
738
|
-
cb(event);
|
|
739
|
-
} catch (err) {
|
|
740
|
-
this.log('error', `Subscription callback error: ${err}`);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
private matchesPattern(pattern: string, eventType: string): boolean {
|
|
748
|
-
if (pattern === '*') return true;
|
|
749
|
-
if (pattern === eventType) return true;
|
|
750
|
-
if (pattern.endsWith(':*')) {
|
|
751
|
-
const prefix = pattern.slice(0, -1);
|
|
752
|
-
return eventType.startsWith(prefix);
|
|
753
|
-
}
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// ─── Internal: Utils ────────────────────────────────────────────────
|
|
758
|
-
|
|
759
|
-
private ensureInitialized(): void {
|
|
760
|
-
if (!this.initialized) {
|
|
761
|
-
throw new Error('BrowserPlayer not initialized — call init() first');
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
private log(level: string, message: string): void {
|
|
766
|
-
if (!this.config.debug && level === 'debug') return;
|
|
767
|
-
const prefix = `[BrowserPlayer]`;
|
|
768
|
-
switch (level) {
|
|
769
|
-
case 'error':
|
|
770
|
-
console.error(prefix, message);
|
|
771
|
-
break;
|
|
772
|
-
case 'warn':
|
|
773
|
-
console.warn(prefix, message);
|
|
774
|
-
break;
|
|
775
|
-
case 'info':
|
|
776
|
-
if (this.config.debug) console.log(prefix, message);
|
|
777
|
-
break;
|
|
778
|
-
case 'debug':
|
|
779
|
-
console.debug(prefix, message);
|
|
780
|
-
break;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|