@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.
Files changed (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.mts +1378 -94
  3. package/dist/index.d.ts +1378 -94
  4. package/dist/index.js +1094 -1309
  5. package/dist/index.mjs +1038 -1296
  6. package/package.json +4 -3
  7. package/package.json.backup +0 -41
  8. package/src/Blueprint.ts +0 -216
  9. package/src/__tests__/Blueprint.test.ts +0 -106
  10. package/src/__tests__/action-context.test.ts +0 -166
  11. package/src/__tests__/actionCreators.test.ts +0 -179
  12. package/src/__tests__/builders.test.ts +0 -336
  13. package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
  14. package/src/__tests__/factories.test.ts +0 -229
  15. package/src/__tests__/loader.test.ts +0 -159
  16. package/src/__tests__/logger.test.ts +0 -70
  17. package/src/__tests__/type-inference.test.ts +0 -160
  18. package/src/__tests__/typed-transitions.test.ts +0 -126
  19. package/src/__tests__/useModuleConfig.test.ts +0 -61
  20. package/src/actionCreators.ts +0 -132
  21. package/src/actions.ts +0 -547
  22. package/src/atoms/index.ts +0 -600
  23. package/src/authoring.ts +0 -92
  24. package/src/browser-player.ts +0 -783
  25. package/src/builders.ts +0 -1342
  26. package/src/components/ExperienceWorkflowBridge.tsx +0 -123
  27. package/src/components/PlayerProvider.tsx +0 -43
  28. package/src/components/atoms/index.tsx +0 -269
  29. package/src/components/index.ts +0 -36
  30. package/src/conditions.ts +0 -692
  31. package/src/config/defineBlueprint.ts +0 -329
  32. package/src/config/defineModel.ts +0 -753
  33. package/src/config/defineWorkspace.ts +0 -24
  34. package/src/core/WorkflowRuntime.ts +0 -153
  35. package/src/factories.ts +0 -425
  36. package/src/grammar/index.ts +0 -173
  37. package/src/hooks/index.ts +0 -106
  38. package/src/hooks/useAuth.ts +0 -288
  39. package/src/hooks/useChannel.ts +0 -304
  40. package/src/hooks/useComputed.ts +0 -154
  41. package/src/hooks/useDomainSubscription.ts +0 -110
  42. package/src/hooks/useDuringAction.ts +0 -99
  43. package/src/hooks/useExperienceState.ts +0 -59
  44. package/src/hooks/useExpressionLibrary.ts +0 -129
  45. package/src/hooks/useForm.ts +0 -352
  46. package/src/hooks/useGeolocation.ts +0 -207
  47. package/src/hooks/useMapView.ts +0 -259
  48. package/src/hooks/useMiddleware.ts +0 -291
  49. package/src/hooks/useModel.ts +0 -363
  50. package/src/hooks/useModule.ts +0 -59
  51. package/src/hooks/useModuleConfig.ts +0 -61
  52. package/src/hooks/useMutation.ts +0 -237
  53. package/src/hooks/useNotification.ts +0 -151
  54. package/src/hooks/useOnChange.ts +0 -30
  55. package/src/hooks/useOnEnter.ts +0 -59
  56. package/src/hooks/useOnEvent.ts +0 -37
  57. package/src/hooks/useOnExit.ts +0 -27
  58. package/src/hooks/useOnTransition.ts +0 -30
  59. package/src/hooks/usePackage.ts +0 -128
  60. package/src/hooks/useParams.ts +0 -33
  61. package/src/hooks/usePlayer.ts +0 -308
  62. package/src/hooks/useQuery.ts +0 -184
  63. package/src/hooks/useRealtimeQuery.ts +0 -222
  64. package/src/hooks/useRole.ts +0 -191
  65. package/src/hooks/useRouteParams.ts +0 -100
  66. package/src/hooks/useRouter.ts +0 -347
  67. package/src/hooks/useServerAction.ts +0 -178
  68. package/src/hooks/useServerState.ts +0 -284
  69. package/src/hooks/useToast.ts +0 -164
  70. package/src/hooks/useTransition.ts +0 -39
  71. package/src/hooks/useView.ts +0 -102
  72. package/src/hooks/useWhileIn.ts +0 -48
  73. package/src/hooks/useWorkflow.ts +0 -63
  74. package/src/index.ts +0 -465
  75. package/src/loader/experience-workflow-loader.ts +0 -192
  76. package/src/loader/index.ts +0 -6
  77. package/src/local/LocalEngine.ts +0 -388
  78. package/src/local/LocalEngineAdapter.ts +0 -175
  79. package/src/local/LocalEngineContext.ts +0 -30
  80. package/src/logger.ts +0 -37
  81. package/src/mixins.ts +0 -1160
  82. package/src/providers/RuntimeContext.ts +0 -20
  83. package/src/providers/WorkflowProvider.tsx +0 -28
  84. package/src/routing/instance-key.ts +0 -107
  85. package/src/server/transition-context.ts +0 -172
  86. package/src/testing/index.ts +0 -9
  87. package/src/testing/useBlueprintTestRunner.ts +0 -91
  88. package/src/testing/useGraphAnalysis.ts +0 -18
  89. package/src/testing/useTestRunner.ts +0 -77
  90. package/src/testing.ts +0 -995
  91. package/src/types/workflow-inference.ts +0 -158
  92. package/src/types.ts +0 -114
  93. package/tsconfig.json +0 -27
  94. package/vitest.config.ts +0 -8
@@ -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
- }