anear-js-api 0.5.9 → 0.6.1

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.
@@ -154,7 +154,11 @@ class AnearEvent extends JsonApiResource {
154
154
  }
155
155
 
156
156
  allowsSpectators() {
157
- return !this.hasFlag("no_spectators")
157
+ //
158
+ // Canonical spectators decision uses the ANAPI-provided boolean when present.
159
+ // Fallback to legacy negative flag for older payloads.
160
+ //
161
+ return this.attributes['spectators-allowed']
158
162
  }
159
163
 
160
164
  isParticipantEventCreator(participant) {
@@ -209,6 +209,16 @@ const AnearParticipantMachineConfig = participantId => ({
209
209
  ]
210
210
  },
211
211
  on: {
212
+ // Even while we're waiting for a reconnect, the AppM may
213
+ // re-trigger a display (e.g. a refreshed PlayableGameBoard).
214
+ // In that case, re-publish the view but keep the timeout
215
+ // semantics governed by the stored remaining time.
216
+ RENDER_DISPLAY: {
217
+ target: '#renderDisplay'
218
+ },
219
+ PRIVATE_DISPLAY: {
220
+ target: '#renderDisplay'
221
+ },
212
222
  PARTICIPANT_EXIT: {
213
223
  target: '#cleanupAndExit'
214
224
  },
@@ -258,6 +268,16 @@ const AnearParticipantMachineConfig = participantId => ({
258
268
  }
259
269
  },
260
270
  on: {
271
+ RENDER_DISPLAY: {
272
+ // Re-render the current prompt for this participant (e.g. after reconnect)
273
+ // without resetting the original timeout start. The updateActionTimeout
274
+ // action will recompute the remaining time based on the original
275
+ // actionTimeoutStart so the participant does not get extra time.
276
+ target: '#renderDisplay'
277
+ },
278
+ PRIVATE_DISPLAY: {
279
+ target: '#renderDisplay'
280
+ },
261
281
  PARTICIPANT_EXIT: {
262
282
  actions: 'logExit',
263
283
  target: '#cleanupAndExit'
@@ -404,22 +424,33 @@ const AnearParticipantMachineFunctions = {
404
424
  lastSeen: context => CurrentDateTimestamp()
405
425
  }),
406
426
  updateActionTimeout: assign((context, event) => {
407
- const newTimeoutDuration = event.data.timeout
427
+ const timeoutFromEvent = event.data.timeout
428
+ const now = CurrentDateTimestamp()
408
429
 
430
+ // If we already have a running timer (e.g. after a disconnect/reconnect
431
+ // cycle or a re-render), treat actionTimeoutMsecs as the remaining
432
+ // duration for this leg and subtract only the time since the last
433
+ // (re)start. This avoids double-subtracting elapsed time across
434
+ // multiple renders.
409
435
  if (context.actionTimeoutStart) {
410
- const elapsed = CurrentDateTimestamp() - context.actionTimeoutStart
411
- const remaining = newTimeoutDuration - elapsed
412
- logger.debug(`[APM] Resuming timer for ${context.anearParticipant.id} with ${remaining}ms remaining`)
413
- return {
414
- actionTimeoutMsecs: remaining > 0 ? remaining : 0
415
- }
416
- } else {
417
- logger.debug(`[APM] Starting new timer for ${context.anearParticipant.id} with ${newTimeoutDuration}ms`)
436
+ const elapsed = now - context.actionTimeoutStart
437
+ const base = context.actionTimeoutMsecs != null ? context.actionTimeoutMsecs : timeoutFromEvent
438
+ const remaining = base - elapsed
439
+ const remainingMsecs = remaining > 0 ? remaining : 0
440
+
441
+ logger.debug(`[APM] Resuming timer for ${context.anearParticipant.id} with ${remainingMsecs}ms remaining`)
418
442
  return {
419
- actionTimeoutMsecs: newTimeoutDuration,
420
- actionTimeoutStart: CurrentDateTimestamp()
443
+ actionTimeoutMsecs: remainingMsecs,
444
+ actionTimeoutStart: now
421
445
  }
422
446
  }
447
+
448
+ // First time this participant sees this prompt: start a fresh timer.
449
+ logger.debug(`[APM] Starting new timer for ${context.anearParticipant.id} with ${timeoutFromEvent}ms`)
450
+ return {
451
+ actionTimeoutMsecs: timeoutFromEvent,
452
+ actionTimeoutStart: now
453
+ }
423
454
  }),
424
455
  nullActionTimeout: assign({
425
456
  actionTimeoutMsecs: _c => null,
@@ -427,12 +458,16 @@ const AnearParticipantMachineFunctions = {
427
458
  }),
428
459
  updateRemainingTimeoutOnDisconnect: assign((context, event) => {
429
460
  if (context.actionTimeoutStart) {
430
- const elapsed = CurrentDateTimestamp() - context.actionTimeoutStart
461
+ const now = CurrentDateTimestamp()
462
+ const elapsed = now - context.actionTimeoutStart
431
463
  const remaining = context.actionTimeoutMsecs - elapsed
432
464
  const remainingMsecs = remaining > 0 ? remaining : 0
433
465
  logger.debug(`[APM] Participant disconnected mid-turn. Storing remaining timeout of ${remainingMsecs}ms`)
434
466
  return {
435
- actionTimeoutMsecs: remainingMsecs
467
+ actionTimeoutMsecs: remainingMsecs,
468
+ // Reset the start baseline to "now" so that subsequent resumptions
469
+ // measure elapsed time from the disconnect point forward.
470
+ actionTimeoutStart: now
436
471
  }
437
472
  }
438
473
  return {}
@@ -336,18 +336,75 @@ class DisplayEventProcessor {
336
336
  let timeout = null
337
337
 
338
338
  const participantStruct = this.participantsIndex.get(participantId)
339
+ const appCtx = templateRenderContext.app || {}
340
+
341
+ // Determine which participant (if any) is currently timed according to
342
+ // the AppM context (e.g., Tic-Tac-Toe currentPlayerToken).
343
+ const currentToken = appCtx.currentPlayerToken
344
+ const currentPlayerId = currentToken && appCtx.playerIds ? appCtx.playerIds[currentToken] : null
345
+ const isCurrentPlayer = currentPlayerId && participantId === currentPlayerId
339
346
 
340
347
  if (timeoutFn) {
341
- timeout = timeoutFn(templateRenderContext.app, participantStruct)
348
+ // Only the current player should receive a real timeout value that
349
+ // starts an APM timer. For other participants we keep `timeout` null
350
+ // so their APMs never start per-participant timers.
351
+ if (isCurrentPlayer) {
352
+ timeout = timeoutFn(appCtx, participantStruct)
353
+ }
342
354
  }
343
355
  const privateRenderContext = {
344
356
  ...templateRenderContext,
345
357
  participant: participantStruct
346
358
  }
347
359
 
348
- if (timeout !== null) {
349
- // APM will compute remaining on its own; start remaining at full msecs here
350
- privateRenderContext.timeout = { msecs: timeout, remainingMsecs: timeout }
360
+ // Build visual timeout (meta.timeout) used by the countdown bar.
361
+ // For the current player, derive from their own APM state.
362
+ // For opponents / other participants, mirror the current player's timer
363
+ // so everyone sees the same countdown without starting extra APM timers.
364
+ let visualTimeout = null
365
+ try {
366
+ // Helper to compute remaining from a given participant machine
367
+ const computeRemainingFromMachine = (pm) => {
368
+ const state = pm?.state
369
+ const ctx = state?.context
370
+ if (!ctx || !ctx.actionTimeoutStart || ctx.actionTimeoutMsecs == null) return null
371
+ const now = Date.now()
372
+ const elapsed = now - ctx.actionTimeoutStart
373
+ const base = ctx.actionTimeoutMsecs
374
+ const remaining = base - elapsed
375
+ return remaining > 0 ? remaining : 0
376
+ }
377
+
378
+ if (isCurrentPlayer) {
379
+ if (timeout !== null) {
380
+ const remainingMsecs = computeRemainingFromMachine(participantMachine) ?? timeout
381
+ visualTimeout = { msecs: timeout, remainingMsecs }
382
+ }
383
+ } else if (currentPlayerId) {
384
+ // Mirror the current player's timer for non-current participants.
385
+ const currentPm = this.participantMachines[currentPlayerId]
386
+ const remainingMsecs = computeRemainingFromMachine(currentPm)
387
+
388
+ if (remainingMsecs != null) {
389
+ // Prefer the app-configured move timeout if available; otherwise
390
+ // fall back to the remaining time as the total for visuals.
391
+ const moveTimeout =
392
+ (appCtx.C && typeof appCtx.C.MOVE_TIMEOUT_MSECS === 'number' && appCtx.C.MOVE_TIMEOUT_MSECS > 0)
393
+ ? appCtx.C.MOVE_TIMEOUT_MSECS
394
+ : remainingMsecs
395
+ visualTimeout = { msecs: moveTimeout, remainingMsecs }
396
+ }
397
+ }
398
+ } catch (_e) {
399
+ // If anything goes wrong computing visualTimeout, we simply omit it and
400
+ // let the templates fall back to their default behavior.
401
+ }
402
+
403
+ if (visualTimeout) {
404
+ privateRenderContext.meta = {
405
+ ...(privateRenderContext.meta || {}),
406
+ timeout: visualTimeout
407
+ }
351
408
  }
352
409
 
353
410
  const privateHtml = template(privateRenderContext)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "0.5.9",
3
+ "version": "0.6.1",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {