@playcademy/sdk 0.0.1-beta.17 → 0.0.1-beta.19

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/dist/index.d.ts CHANGED
@@ -5,5 +5,5 @@
5
5
  * providing access to the main client class and supporting utilities.
6
6
  */
7
7
  export { PlaycademyClient } from './core/client';
8
- export { bus, BusEvents } from './bus';
8
+ export { messaging, MessageEvents } from './messaging';
9
9
  export type * from './types';
package/dist/index.js CHANGED
@@ -103,60 +103,96 @@ function createAuthNamespace(client) {
103
103
  };
104
104
  }
105
105
 
106
- // src/bus.ts
107
- var BusEvents, busListeners, bus;
108
- var init_bus = __esm(() => {
109
- ((BusEvents2) => {
110
- BusEvents2["INIT"] = "PLAYCADEMY_INIT";
111
- BusEvents2["TOKEN_REFRESH"] = "PLAYCADEMY_TOKEN_REFRESH";
112
- BusEvents2["PAUSE"] = "PLAYCADEMY_PAUSE";
113
- BusEvents2["RESUME"] = "PLAYCADEMY_RESUME";
114
- BusEvents2["FORCE_EXIT"] = "PLAYCADEMY_FORCE_EXIT";
115
- BusEvents2["OVERLAY"] = "PLAYCADEMY_OVERLAY";
116
- BusEvents2["READY"] = "PLAYCADEMY_READY";
117
- BusEvents2["EXIT"] = "PLAYCADEMY_EXIT";
118
- BusEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
119
- })(BusEvents ||= {});
120
- busListeners = new Map;
121
- bus = {
122
- emit(type, payload) {
123
- const isIframe = typeof window !== "undefined" && window.self !== window.top;
124
- const iframeToParentEvents = [
125
- "PLAYCADEMY_READY" /* READY */,
126
- "PLAYCADEMY_EXIT" /* EXIT */,
127
- "PLAYCADEMY_TELEMETRY" /* TELEMETRY */
128
- ];
129
- if (isIframe && iframeToParentEvents.includes(type)) {
130
- let messageData = { type };
131
- if (payload !== undefined && typeof payload === "object" && payload !== null) {
132
- messageData = { ...messageData, ...payload };
133
- }
134
- window.parent.postMessage(messageData, "*");
135
- } else {
136
- window.dispatchEvent(new CustomEvent(type, { detail: payload }));
137
- }
138
- },
139
- on(type, handler) {
140
- const listener = (event) => handler(event.detail);
141
- if (!busListeners.has(type)) {
142
- busListeners.set(type, new Map);
143
- }
144
- busListeners.get(type).set(handler, listener);
145
- window.addEventListener(type, listener);
146
- },
147
- off(type, handler) {
148
- const typeListeners = busListeners.get(type);
149
- if (!typeListeners || !typeListeners.has(handler)) {
150
- return;
151
- }
152
- const actualListener = typeListeners.get(handler);
153
- window.removeEventListener(type, actualListener);
154
- typeListeners.delete(handler);
155
- if (typeListeners.size === 0) {
156
- busListeners.delete(type);
106
+ // src/messaging.ts
107
+ class PlaycademyMessaging {
108
+ listeners = new Map;
109
+ send(type, payload, options) {
110
+ if (options?.target) {
111
+ this.sendViaPostMessage(type, payload, options.target, options.origin || "*");
112
+ return;
113
+ }
114
+ const context = this.getMessagingContext(type);
115
+ if (context.shouldUsePostMessage) {
116
+ this.sendViaPostMessage(type, payload, context.target, context.origin);
117
+ } else {
118
+ this.sendViaCustomEvent(type, payload);
119
+ }
120
+ }
121
+ listen(type, handler) {
122
+ const postMessageListener = (event) => {
123
+ const messageEvent = event;
124
+ if (messageEvent.data?.type === type) {
125
+ handler(messageEvent.data.payload || messageEvent.data);
157
126
  }
127
+ };
128
+ const customEventListener = (event) => {
129
+ handler(event.detail);
130
+ };
131
+ if (!this.listeners.has(type)) {
132
+ this.listeners.set(type, new Map);
158
133
  }
159
- };
134
+ const listenerMap = this.listeners.get(type);
135
+ listenerMap.set(handler, {
136
+ postMessage: postMessageListener,
137
+ customEvent: customEventListener
138
+ });
139
+ window.addEventListener("message", postMessageListener);
140
+ window.addEventListener(type, customEventListener);
141
+ }
142
+ unlisten(type, handler) {
143
+ const typeListeners = this.listeners.get(type);
144
+ if (!typeListeners || !typeListeners.has(handler)) {
145
+ return;
146
+ }
147
+ const listeners = typeListeners.get(handler);
148
+ window.removeEventListener("message", listeners.postMessage);
149
+ window.removeEventListener(type, listeners.customEvent);
150
+ typeListeners.delete(handler);
151
+ if (typeListeners.size === 0) {
152
+ this.listeners.delete(type);
153
+ }
154
+ }
155
+ getMessagingContext(eventType) {
156
+ const isIframe = typeof window !== "undefined" && window.self !== window.top;
157
+ const iframeToParentEvents = [
158
+ "PLAYCADEMY_READY" /* READY */,
159
+ "PLAYCADEMY_EXIT" /* EXIT */,
160
+ "PLAYCADEMY_TELEMETRY" /* TELEMETRY */
161
+ ];
162
+ const shouldUsePostMessage = isIframe && iframeToParentEvents.includes(eventType);
163
+ return {
164
+ shouldUsePostMessage,
165
+ target: shouldUsePostMessage ? window.parent : undefined,
166
+ origin: "*"
167
+ };
168
+ }
169
+ sendViaPostMessage(type, payload, target = window.parent, origin = "*") {
170
+ let messageData = { type };
171
+ if (payload !== undefined && typeof payload === "object" && payload !== null) {
172
+ messageData = { ...messageData, ...payload };
173
+ } else if (payload !== undefined) {
174
+ messageData.payload = payload;
175
+ }
176
+ target.postMessage(messageData, origin);
177
+ }
178
+ sendViaCustomEvent(type, payload) {
179
+ window.dispatchEvent(new CustomEvent(type, { detail: payload }));
180
+ }
181
+ }
182
+ var MessageEvents, messaging;
183
+ var init_messaging = __esm(() => {
184
+ ((MessageEvents2) => {
185
+ MessageEvents2["INIT"] = "PLAYCADEMY_INIT";
186
+ MessageEvents2["TOKEN_REFRESH"] = "PLAYCADEMY_TOKEN_REFRESH";
187
+ MessageEvents2["PAUSE"] = "PLAYCADEMY_PAUSE";
188
+ MessageEvents2["RESUME"] = "PLAYCADEMY_RESUME";
189
+ MessageEvents2["FORCE_EXIT"] = "PLAYCADEMY_FORCE_EXIT";
190
+ MessageEvents2["OVERLAY"] = "PLAYCADEMY_OVERLAY";
191
+ MessageEvents2["READY"] = "PLAYCADEMY_READY";
192
+ MessageEvents2["EXIT"] = "PLAYCADEMY_EXIT";
193
+ MessageEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
194
+ })(MessageEvents ||= {});
195
+ messaging = new PlaycademyMessaging;
160
196
  });
161
197
 
162
198
  // src/core/namespaces/runtime.ts
@@ -177,12 +213,12 @@ function createRuntimeNamespace(client) {
177
213
  console.error("[SDK] Failed to auto-end session:", client["internalClientSessionId"], error);
178
214
  }
179
215
  }
180
- bus.emit("PLAYCADEMY_EXIT" /* EXIT */, undefined);
216
+ messaging.send("PLAYCADEMY_EXIT" /* EXIT */, undefined);
181
217
  }
182
218
  };
183
219
  }
184
220
  var init_runtime = __esm(() => {
185
- init_bus();
221
+ init_messaging();
186
222
  });
187
223
 
188
224
  // src/core/namespaces/games.ts
@@ -394,15 +430,15 @@ async function init() {
394
430
  token: config.token,
395
431
  gameId: config.gameId
396
432
  });
397
- bus.on("PLAYCADEMY_TOKEN_REFRESH" /* TOKEN_REFRESH */, ({ token }) => client.setToken(token));
398
- bus.emit("PLAYCADEMY_READY" /* READY */, undefined);
433
+ messaging.listen("PLAYCADEMY_TOKEN_REFRESH" /* TOKEN_REFRESH */, ({ token }) => client.setToken(token));
434
+ messaging.send("PLAYCADEMY_READY" /* READY */, undefined);
399
435
  if (import.meta.env?.MODE === "development") {
400
436
  window.PLAYCADEMY_CLIENT = client;
401
437
  }
402
438
  return client;
403
439
  }
404
440
  var init_init = __esm(() => {
405
- init_bus();
441
+ init_messaging();
406
442
  });
407
443
 
408
444
  // src/core/static/login.ts
@@ -542,9 +578,9 @@ var init_client = __esm(() => {
542
578
 
543
579
  // src/index.ts
544
580
  init_client();
545
- init_bus();
581
+ init_messaging();
546
582
  export {
547
- bus,
583
+ messaging,
548
584
  PlaycademyClient,
549
- BusEvents
585
+ MessageEvents
550
586
  };
@@ -0,0 +1,510 @@
1
+ /**
2
+ * @fileoverview Playcademy Messaging System
3
+ *
4
+ * This file implements a unified messaging system for the Playcademy platform that handles
5
+ * communication between different contexts:
6
+ *
7
+ * 1. **Iframe-to-Parent Communication**: When games run inside iframes (production/development),
8
+ * they need to communicate with the parent window using postMessage API
9
+ *
10
+ * 2. **Local Communication**: When games run in the same context (local development),
11
+ * they use CustomEvents for internal messaging
12
+ *
13
+ * The system automatically detects the runtime environment and chooses the appropriate
14
+ * transport method, abstracting this complexity from the developer.
15
+ *
16
+ * **Architecture Overview**:
17
+ * - Games run in iframes for security and isolation
18
+ * - Parent window (Playcademy shell) manages game lifecycle
19
+ * - Messages flow bidirectionally between parent and iframe
20
+ * - Local development mode simulates this architecture without iframes
21
+ */
22
+ import type { GameContextPayload } from './types';
23
+ /**
24
+ * Enumeration of all message types used in the Playcademy messaging system.
25
+ *
26
+ * **Message Flow Patterns**:
27
+ *
28
+ * **Parent → Game (Overworld → Game)**:
29
+ * - INIT: Provides game with authentication token and configuration
30
+ * - TOKEN_REFRESH: Updates game's authentication token before expiry
31
+ * - PAUSE/RESUME: Controls game execution state
32
+ * - FORCE_EXIT: Immediately terminates the game
33
+ * - OVERLAY: Shows/hides UI overlays over the game
34
+ *
35
+ * **Game → Parent (Game → Overworld)**:
36
+ * - READY: Game has loaded and is ready to receive messages
37
+ * - EXIT: Game requests to be closed (user clicked exit, game ended, etc.)
38
+ * - TELEMETRY: Game reports performance metrics (FPS, memory usage, etc.)
39
+ */
40
+ export declare enum MessageEvents {
41
+ /**
42
+ * Initializes the game with authentication context and configuration.
43
+ * Sent immediately after game iframe loads.
44
+ * Payload: { baseUrl: string, token: string, gameId: string }
45
+ */
46
+ INIT = "PLAYCADEMY_INIT",
47
+ /**
48
+ * Updates the game's authentication token before it expires.
49
+ * Sent periodically to maintain valid authentication.
50
+ * Payload: { token: string, exp: number }
51
+ */
52
+ TOKEN_REFRESH = "PLAYCADEMY_TOKEN_REFRESH",
53
+ /**
54
+ * Pauses game execution (e.g., when user switches tabs).
55
+ * Game should pause timers, animations, and user input.
56
+ * Payload: void
57
+ */
58
+ PAUSE = "PLAYCADEMY_PAUSE",
59
+ /**
60
+ * Resumes game execution after being paused.
61
+ * Game should restore timers, animations, and user input.
62
+ * Payload: void
63
+ */
64
+ RESUME = "PLAYCADEMY_RESUME",
65
+ /**
66
+ * Forces immediate game termination (emergency exit).
67
+ * Game should clean up resources and exit immediately.
68
+ * Payload: void
69
+ */
70
+ FORCE_EXIT = "PLAYCADEMY_FORCE_EXIT",
71
+ /**
72
+ * Shows or hides UI overlays over the game.
73
+ * Game may need to pause or adjust rendering accordingly.
74
+ * Payload: boolean (true = show overlay, false = hide overlay)
75
+ */
76
+ OVERLAY = "PLAYCADEMY_OVERLAY",
77
+ /**
78
+ * Game has finished loading and is ready to receive messages.
79
+ * Sent once after game initialization is complete.
80
+ * Payload: void
81
+ */
82
+ READY = "PLAYCADEMY_READY",
83
+ /**
84
+ * Game requests to be closed/exited.
85
+ * Sent when user clicks exit button or game naturally ends.
86
+ * Payload: void
87
+ */
88
+ EXIT = "PLAYCADEMY_EXIT",
89
+ /**
90
+ * Game reports performance telemetry data.
91
+ * Sent periodically for monitoring and analytics.
92
+ * Payload: { fps: number, mem: number }
93
+ */
94
+ TELEMETRY = "PLAYCADEMY_TELEMETRY"
95
+ }
96
+ /**
97
+ * Type definition for message handler functions.
98
+ * These functions are called when a specific message type is received.
99
+ *
100
+ * @template T - The type of payload data the handler expects
101
+ * @param payload - The data sent with the message
102
+ */
103
+ type MessageHandler<T = unknown> = (payload: T) => void;
104
+ /**
105
+ * Type mapping that defines the payload structure for each message type.
106
+ * This ensures type safety when sending and receiving messages.
107
+ *
108
+ * **Usage Examples**:
109
+ * ```typescript
110
+ * // Type-safe message sending
111
+ * messaging.send(MessageEvents.INIT, { baseUrl: '/api', token: 'abc', gameId: '123' })
112
+ *
113
+ * // Type-safe message handling
114
+ * messaging.listen(MessageEvents.TOKEN_REFRESH, ({ token, exp }) => {
115
+ * console.log(`New token expires at: ${new Date(exp)}`)
116
+ * })
117
+ * ```
118
+ */
119
+ export type MessageEventMap = {
120
+ /** Game initialization context with API endpoint, auth token, and game ID */
121
+ [MessageEvents.INIT]: GameContextPayload;
122
+ /** Token refresh data with new token and expiration timestamp */
123
+ [MessageEvents.TOKEN_REFRESH]: {
124
+ token: string;
125
+ exp: number;
126
+ };
127
+ /** Pause message has no payload data */
128
+ [MessageEvents.PAUSE]: void;
129
+ /** Resume message has no payload data */
130
+ [MessageEvents.RESUME]: void;
131
+ /** Force exit message has no payload data */
132
+ [MessageEvents.FORCE_EXIT]: void;
133
+ /** Overlay visibility state (true = show, false = hide) */
134
+ [MessageEvents.OVERLAY]: boolean;
135
+ /** Ready message has no payload data */
136
+ [MessageEvents.READY]: void;
137
+ /** Exit message has no payload data */
138
+ [MessageEvents.EXIT]: void;
139
+ /** Performance telemetry data */
140
+ [MessageEvents.TELEMETRY]: {
141
+ fps: number;
142
+ mem: number;
143
+ };
144
+ };
145
+ /**
146
+ * **PlaycademyMessaging Class**
147
+ *
148
+ * This is the core messaging system that handles all communication in the Playcademy platform.
149
+ * It automatically detects the runtime environment and chooses the appropriate transport method.
150
+ *
151
+ * **Key Features**:
152
+ * 1. **Automatic Transport Selection**: Detects iframe vs local context and uses appropriate method
153
+ * 2. **Type Safety**: Full TypeScript support with payload type checking
154
+ * 3. **Bidirectional Communication**: Handles both parent→game and game→parent messages
155
+ * 4. **Event Cleanup**: Proper listener management to prevent memory leaks
156
+ *
157
+ * **Transport Methods**:
158
+ * - **postMessage**: Used for iframe-to-parent communication (production/development)
159
+ * - **CustomEvent**: Used for local same-context communication (local development)
160
+ *
161
+ * **Runtime Detection Logic**:
162
+ * - If `window.self !== window.top`: We're in an iframe, use postMessage
163
+ * - If `window.self === window.top`: We're in the same context, use CustomEvent
164
+ *
165
+ * **Example Usage**:
166
+ * ```typescript
167
+ * // Send a message (automatically chooses transport)
168
+ * messaging.send(MessageEvents.READY, undefined)
169
+ *
170
+ * // Listen for messages (handles both transports)
171
+ * messaging.listen(MessageEvents.INIT, (payload) => {
172
+ * console.log('Game initialized with:', payload)
173
+ * })
174
+ *
175
+ * // Clean up listeners
176
+ * messaging.unlisten(MessageEvents.INIT, handler)
177
+ * ```
178
+ */
179
+ declare class PlaycademyMessaging {
180
+ /**
181
+ * Internal storage for message listeners.
182
+ *
183
+ * **Structure Explanation**:
184
+ * - Outer Map: MessageEvents → Map of handlers for that event type
185
+ * - Inner Map: MessageHandler → Object containing both listener types
186
+ * - Object: { postMessage: EventListener, customEvent: EventListener }
187
+ *
188
+ * **Why Two Listeners Per Handler?**:
189
+ * Since we don't know at registration time which transport will be used,
190
+ * we register both a postMessage listener and a CustomEvent listener.
191
+ * The appropriate one will be triggered based on the runtime context.
192
+ */
193
+ private listeners;
194
+ /**
195
+ * **Send Message Method**
196
+ *
197
+ * Sends a message using the appropriate transport method based on the runtime context.
198
+ * This is the main public API for sending messages in the Playcademy system.
199
+ *
200
+ * **How It Works**:
201
+ * 1. If options.target is provided, always use postMessage to that target
202
+ * 2. Otherwise, analyzes the message type and current runtime context
203
+ * 3. Determines if we're in an iframe and if this message should use postMessage
204
+ * 4. Routes to the appropriate transport method (postMessage or CustomEvent)
205
+ *
206
+ * **Transport Selection Logic**:
207
+ * - **postMessage**: Used when target is specified OR when game is in iframe sending to parent
208
+ * - **CustomEvent**: Used for local/same-context communication
209
+ *
210
+ * **Type Safety**:
211
+ * The generic type parameter `K` ensures that the payload type matches
212
+ * the expected type for the given message event.
213
+ *
214
+ * @template K - The message event type (ensures type safety)
215
+ * @param type - The message event type to send
216
+ * @param payload - The data to send with the message (type-checked)
217
+ * @param options - Optional configuration for target window and origin
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * // Send game ready signal (auto-detected transport)
222
+ * messaging.send(MessageEvents.READY, undefined)
223
+ *
224
+ * // Send to specific iframe (parent → game)
225
+ * messaging.send(MessageEvents.INIT, { baseUrl, token, gameId }, {
226
+ * target: iframe.contentWindow,
227
+ * origin: 'https://game.example.com'
228
+ * })
229
+ *
230
+ * // Send telemetry data (auto-detected transport)
231
+ * messaging.send(MessageEvents.TELEMETRY, { fps: 60, mem: 128 })
232
+ * ```
233
+ */
234
+ send<K extends MessageEvents>(type: K, payload: MessageEventMap[K], options?: {
235
+ target?: Window;
236
+ origin?: string;
237
+ }): void;
238
+ /**
239
+ * **Listen for Messages Method**
240
+ *
241
+ * Registers a message listener that will be called when the specified message type is received.
242
+ * This method automatically handles both postMessage and CustomEvent sources.
243
+ *
244
+ * **Why Register Two Listeners?**:
245
+ * Since we don't know at registration time which transport method will be used to send
246
+ * messages, we register listeners for both possible sources:
247
+ * 1. **postMessage listener**: Handles messages from iframe-to-parent communication
248
+ * 2. **CustomEvent listener**: Handles messages from local same-context communication
249
+ *
250
+ * **Message Processing**:
251
+ * - **postMessage**: Extracts data from `event.data.payload` or `event.data`
252
+ * - **CustomEvent**: Extracts data from `event.detail`
253
+ *
254
+ * **Type Safety**:
255
+ * The handler function receives the correctly typed payload based on the message type.
256
+ *
257
+ * @template K - The message event type (ensures type safety)
258
+ * @param type - The message event type to listen for
259
+ * @param handler - Function to call when the message is received
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * // Listen for game initialization
264
+ * messaging.listen(MessageEvents.INIT, (payload) => {
265
+ * // payload is automatically typed as GameContextPayload
266
+ * console.log(`Game ${payload.gameId} initialized`)
267
+ * console.log(`API base URL: ${payload.baseUrl}`)
268
+ * })
269
+ *
270
+ * // Listen for token refresh
271
+ * messaging.listen(MessageEvents.TOKEN_REFRESH, ({ token, exp }) => {
272
+ * // payload is automatically typed as { token: string; exp: number }
273
+ * updateAuthToken(token)
274
+ * scheduleTokenRefresh(exp)
275
+ * })
276
+ * ```
277
+ */
278
+ listen<K extends MessageEvents>(type: K, handler: MessageHandler<MessageEventMap[K]>): void;
279
+ /**
280
+ * **Remove Message Listener Method**
281
+ *
282
+ * Removes a previously registered message listener to prevent memory leaks and unwanted callbacks.
283
+ * This method cleans up both the postMessage and CustomEvent listeners that were registered.
284
+ *
285
+ * **Why Clean Up Both Listeners?**:
286
+ * When we registered the listener with `listen()`, we created two browser event listeners:
287
+ * 1. A 'message' event listener for postMessage communication
288
+ * 2. A custom event listener for local CustomEvent communication
289
+ *
290
+ * Both must be removed to prevent memory leaks and ensure the handler is completely unregistered.
291
+ *
292
+ * **Memory Management**:
293
+ * - Removes both event listeners from the browser
294
+ * - Cleans up internal tracking maps
295
+ * - If no more handlers exist for a message type, removes the entire type entry
296
+ *
297
+ * **Safe to Call Multiple Times**:
298
+ * This method is idempotent - calling it multiple times with the same handler is safe.
299
+ *
300
+ * @template K - The message event type (ensures type safety)
301
+ * @param type - The message event type to stop listening for
302
+ * @param handler - The exact handler function that was passed to listen()
303
+ *
304
+ * @example
305
+ * ```typescript
306
+ * // Register a handler
307
+ * const handleInit = (payload) => console.log('Game initialized')
308
+ * messaging.listen(MessageEvents.INIT, handleInit)
309
+ *
310
+ * // Later, remove the handler
311
+ * messaging.unlisten(MessageEvents.INIT, handleInit)
312
+ *
313
+ * // Safe to call multiple times
314
+ * messaging.unlisten(MessageEvents.INIT, handleInit) // No error
315
+ * ```
316
+ */
317
+ unlisten<K extends MessageEvents>(type: K, handler: MessageHandler<MessageEventMap[K]>): void;
318
+ /**
319
+ * **Get Messaging Context Method**
320
+ *
321
+ * Analyzes the current runtime environment and message type to determine the appropriate
322
+ * transport method and configuration for sending messages.
323
+ *
324
+ * **Runtime Environment Detection**:
325
+ * The method detects whether the code is running in an iframe by comparing:
326
+ * - `window.self`: Reference to the current window
327
+ * - `window.top`: Reference to the topmost window in the hierarchy
328
+ *
329
+ * If they're different, we're in an iframe. If they're the same, we're in the top-level window.
330
+ *
331
+ * **Message Direction Analysis**:
332
+ * Different message types flow in different directions:
333
+ * - **Game → Parent**: READY, EXIT, TELEMETRY (use postMessage when in iframe)
334
+ * - **Parent → Game**: INIT, TOKEN_REFRESH, PAUSE, etc. (use CustomEvent in local dev)
335
+ *
336
+ * **Cross-Context Communication**:
337
+ * This messaging system now handles ALL communication patterns:
338
+ * - Game → Parent: Automatic detection and postMessage
339
+ * - Parent → Game: Use options.target to specify the iframe window
340
+ * - Local development: Automatic CustomEvent fallback
341
+ *
342
+ * **Transport Selection Logic**:
343
+ * - **postMessage**: Used when game is in iframe AND sending to parent
344
+ * - **CustomEvent**: Used for all other cases (local development, parent-to-game)
345
+ *
346
+ * **Security Considerations**:
347
+ * The origin is currently set to '*' for development convenience, but should be
348
+ * configurable for production security.
349
+ *
350
+ * @param eventType - The message event type being sent
351
+ * @returns Configuration object with transport method and target information
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * // In iframe sending READY to parent
356
+ * const context = getMessagingContext(MessageEvents.READY)
357
+ * // Returns: { shouldUsePostMessage: true, target: window.parent, origin: '*' }
358
+ *
359
+ * // In same context sending INIT
360
+ * const context = getMessagingContext(MessageEvents.INIT)
361
+ * // Returns: { shouldUsePostMessage: false, target: undefined, origin: '*' }
362
+ * ```
363
+ */
364
+ private getMessagingContext;
365
+ /**
366
+ * **Send Via PostMessage Method**
367
+ *
368
+ * Sends a message using the browser's postMessage API for iframe-to-parent communication.
369
+ * This method is used when the game is running in an iframe and needs to communicate
370
+ * with the parent window (the Playcademy shell).
371
+ *
372
+ * **PostMessage Protocol**:
373
+ * The postMessage API is the standard way for iframes to communicate with their parent.
374
+ * It's secure, cross-origin capable, and designed specifically for this use case.
375
+ *
376
+ * **Message Structure**:
377
+ * The method creates a message object with the following structure:
378
+ * - `type`: The message event type (e.g., 'PLAYCADEMY_READY')
379
+ * - Payload data: Either spread into the object or in a `payload` property
380
+ *
381
+ * **Payload Handling**:
382
+ * - **Object payloads**: Spread directly into the message (e.g., { type, token, exp })
383
+ * - **Primitive payloads**: Wrapped in a `payload` property (e.g., { type, payload: true })
384
+ * - **Undefined payloads**: Only the type is sent (e.g., { type })
385
+ *
386
+ * **Security**:
387
+ * The origin parameter controls which domains can receive the message.
388
+ * Currently set to '*' for development, but should be restricted in production.
389
+ *
390
+ * @template K - The message event type (ensures type safety)
391
+ * @param type - The message event type to send
392
+ * @param payload - The data to send with the message
393
+ * @param target - The target window (defaults to parent window)
394
+ * @param origin - The allowed origin for the message (defaults to '*')
395
+ *
396
+ * @example
397
+ * ```typescript
398
+ * // Send ready signal (no payload)
399
+ * sendViaPostMessage(MessageEvents.READY, undefined)
400
+ * // Sends: { type: 'PLAYCADEMY_READY' }
401
+ *
402
+ * // Send telemetry data (object payload)
403
+ * sendViaPostMessage(MessageEvents.TELEMETRY, { fps: 60, mem: 128 })
404
+ * // Sends: { type: 'PLAYCADEMY_TELEMETRY', fps: 60, mem: 128 }
405
+ *
406
+ * // Send overlay state (primitive payload)
407
+ * sendViaPostMessage(MessageEvents.OVERLAY, true)
408
+ * // Sends: { type: 'PLAYCADEMY_OVERLAY', payload: true }
409
+ * ```
410
+ */
411
+ private sendViaPostMessage;
412
+ /**
413
+ * **Send Via CustomEvent Method**
414
+ *
415
+ * Sends a message using the browser's CustomEvent API for local same-context communication.
416
+ * This method is used when both the sender and receiver are in the same window context,
417
+ * typically during local development or when the parent needs to send messages to the game.
418
+ *
419
+ * **CustomEvent Protocol**:
420
+ * CustomEvent is a browser API for creating and dispatching custom events within the same
421
+ * window context. It's simpler than postMessage but only works within the same origin/context.
422
+ *
423
+ * **Event Structure**:
424
+ * - **type**: The event name (e.g., 'PLAYCADEMY_INIT')
425
+ * - **detail**: The payload data attached to the event
426
+ *
427
+ * **When This Is Used**:
428
+ * - Local development when game and shell run in the same context
429
+ * - Parent-to-game communication (INIT, TOKEN_REFRESH, PAUSE, etc.)
430
+ * - Any scenario where postMessage isn't needed
431
+ *
432
+ * **Event Bubbling**:
433
+ * CustomEvents are dispatched on the window object and can be listened to by any
434
+ * code in the same context using `addEventListener(type, handler)`.
435
+ *
436
+ * @template K - The message event type (ensures type safety)
437
+ * @param type - The message event type to send (becomes the event name)
438
+ * @param payload - The data to send with the message (stored in event.detail)
439
+ *
440
+ * @example
441
+ * ```typescript
442
+ * // Send initialization data
443
+ * sendViaCustomEvent(MessageEvents.INIT, {
444
+ * baseUrl: '/api',
445
+ * token: 'abc123',
446
+ * gameId: 'game-456'
447
+ * })
448
+ * // Creates: CustomEvent('PLAYCADEMY_INIT', { detail: { baseUrl, token, gameId } })
449
+ *
450
+ * // Send pause signal
451
+ * sendViaCustomEvent(MessageEvents.PAUSE, undefined)
452
+ * // Creates: CustomEvent('PLAYCADEMY_PAUSE', { detail: undefined })
453
+ *
454
+ * // Listeners can access the data via event.detail:
455
+ * window.addEventListener('PLAYCADEMY_INIT', (event) => {
456
+ * console.log(event.detail.gameId) // 'game-456'
457
+ * })
458
+ * ```
459
+ */
460
+ private sendViaCustomEvent;
461
+ }
462
+ /**
463
+ * **Playcademy Messaging Singleton**
464
+ *
465
+ * This is the main messaging instance used throughout the Playcademy platform.
466
+ * It's exported as a singleton to ensure consistent communication across all parts
467
+ * of the application.
468
+ *
469
+ * **Why a Singleton?**:
470
+ * - Ensures all parts of the app use the same messaging instance
471
+ * - Prevents conflicts between multiple messaging systems
472
+ * - Simplifies the API - no need to pass instances around
473
+ * - Maintains consistent event listener management
474
+ *
475
+ * **Usage in Different Contexts**:
476
+ *
477
+ * **In Games**:
478
+ * ```typescript
479
+ * import { messaging, MessageEvents } from '@playcademy/sdk'
480
+ *
481
+ * // Tell parent we're ready
482
+ * messaging.send(MessageEvents.READY, undefined)
483
+ *
484
+ * // Listen for pause/resume
485
+ * messaging.listen(MessageEvents.PAUSE, () => game.pause())
486
+ * messaging.listen(MessageEvents.RESUME, () => game.resume())
487
+ * ```
488
+ *
489
+ * **In Parent Shell**:
490
+ * ```typescript
491
+ * import { messaging, MessageEvents } from '@playcademy/sdk'
492
+ *
493
+ * // Send initialization data to game
494
+ * messaging.send(MessageEvents.INIT, { baseUrl, token, gameId })
495
+ *
496
+ * // Listen for game events
497
+ * messaging.listen(MessageEvents.EXIT, () => closeGame())
498
+ * messaging.listen(MessageEvents.READY, () => showGame())
499
+ * ```
500
+ *
501
+ * **Automatic Transport Selection**:
502
+ * The messaging system automatically chooses the right transport method:
503
+ * - Uses postMessage when game is in iframe sending to parent
504
+ * - Uses CustomEvent for local development and parent-to-game communication
505
+ *
506
+ * **Type Safety**:
507
+ * All message sending and receiving is fully type-safe with TypeScript.
508
+ */
509
+ export declare const messaging: PlaycademyMessaging;
510
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@playcademy/sdk",
3
3
  "type": "module",
4
- "version": "0.0.1-beta.17",
4
+ "version": "0.0.1-beta.19",
5
5
  "exports": {
6
6
  ".": {
7
7
  "import": "./dist/index.js",
package/dist/bus.d.ts DELETED
@@ -1,37 +0,0 @@
1
- import type { GameContextPayload } from './types';
2
- export declare enum BusEvents {
3
- INIT = "PLAYCADEMY_INIT",
4
- TOKEN_REFRESH = "PLAYCADEMY_TOKEN_REFRESH",
5
- PAUSE = "PLAYCADEMY_PAUSE",
6
- RESUME = "PLAYCADEMY_RESUME",
7
- FORCE_EXIT = "PLAYCADEMY_FORCE_EXIT",
8
- OVERLAY = "PLAYCADEMY_OVERLAY",
9
- READY = "PLAYCADEMY_READY",
10
- EXIT = "PLAYCADEMY_EXIT",
11
- TELEMETRY = "PLAYCADEMY_TELEMETRY"
12
- }
13
- type BusHandler<T = unknown> = (payload: T) => void;
14
- export type BusEventMap = {
15
- [BusEvents.INIT]: GameContextPayload;
16
- [BusEvents.TOKEN_REFRESH]: {
17
- token: string;
18
- exp: number;
19
- };
20
- [BusEvents.PAUSE]: void;
21
- [BusEvents.RESUME]: void;
22
- [BusEvents.FORCE_EXIT]: void;
23
- [BusEvents.OVERLAY]: boolean;
24
- [BusEvents.READY]: void;
25
- [BusEvents.EXIT]: void;
26
- [BusEvents.TELEMETRY]: {
27
- fps: number;
28
- mem: number;
29
- };
30
- };
31
- interface Bus {
32
- emit<K extends BusEvents>(type: K, payload: BusEventMap[K]): void;
33
- on<K extends BusEvents>(type: K, handler: BusHandler<BusEventMap[K]>): void;
34
- off<K extends BusEvents>(type: K, handler: BusHandler<BusEventMap[K]>): void;
35
- }
36
- export declare const bus: Bus;
37
- export {};