anear-js-api 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -61,6 +61,24 @@ const C = require('../utils/Constants')
|
|
|
61
61
|
const CreateEventChannelNameTemplate = appId => `anear:${appId}:e`
|
|
62
62
|
const DefaultTemplatesRootDir = "./views"
|
|
63
63
|
|
|
64
|
+
// Process-wide signal handling.
|
|
65
|
+
// We install exactly once per process to avoid duplicate handlers in reconnect/restart scenarios.
|
|
66
|
+
let _sigtermHandlerInstalled = false
|
|
67
|
+
let _exitScheduled = false
|
|
68
|
+
|
|
69
|
+
function scheduleExit(code, message) {
|
|
70
|
+
if (_exitScheduled) return
|
|
71
|
+
_exitScheduled = true
|
|
72
|
+
|
|
73
|
+
if (message) logger.info(`${message} (pid=${process.pid})`)
|
|
74
|
+
|
|
75
|
+
// Give file logger a moment to flush (simple-node-logger writes to a stream).
|
|
76
|
+
// `process.exit()` exits immediately and can drop buffered writes.
|
|
77
|
+
const delayMs = 200
|
|
78
|
+
process.exitCode = code
|
|
79
|
+
setTimeout(() => process.exit(code), delayMs)
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
const AnearCoreServiceMachineContext = (appId, appEventMachineFactory, appParticipantMachineFactory) => ({
|
|
65
83
|
appId,
|
|
66
84
|
appData: null,
|
|
@@ -71,6 +89,7 @@ const AnearCoreServiceMachineContext = (appId, appEventMachineFactory, appPartic
|
|
|
71
89
|
imageAssetsUrl: null,
|
|
72
90
|
fontAssetsUrl: null,
|
|
73
91
|
newEventCreationChannel: null,
|
|
92
|
+
shutdownRequested: false, // First SIGTERM received; stop accepting new events
|
|
74
93
|
retryDelay: 0
|
|
75
94
|
})
|
|
76
95
|
|
|
@@ -78,11 +97,22 @@ const AnearCoreServiceMachineConfig = appId => ({
|
|
|
78
97
|
id: `AnearCoreServiceMachine_${appId}`,
|
|
79
98
|
initial: 'fetchAppDataWithRetry',
|
|
80
99
|
context: ({ input }) => input,
|
|
100
|
+
on: {
|
|
101
|
+
SIGTERM: {
|
|
102
|
+
actions: [
|
|
103
|
+
'logSigtermReceived',
|
|
104
|
+
'hardExitIfSecondSigterm',
|
|
105
|
+
'markShutdownRequested',
|
|
106
|
+
'detachCreateEventChannel',
|
|
107
|
+
'exitIfNoActiveEvents'
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
},
|
|
81
111
|
states: {
|
|
82
112
|
fetchAppDataWithRetry: {
|
|
83
113
|
entry: ({ context }) => {
|
|
84
114
|
const appId = context.appId || process.env.ANEARAPP_APP_ID || 'unknown'
|
|
85
|
-
logger.info(`[ACSM] ===== Booting App (${appId}) on anear-js-api version ${anearJsApiVersion} =====`)
|
|
115
|
+
logger.info(`[ACSM] ===== Booting App (${appId}) on anear-js-api version ${anearJsApiVersion} pid=${process.pid} =====`)
|
|
86
116
|
},
|
|
87
117
|
initial: 'fetchAppData',
|
|
88
118
|
states: {
|
|
@@ -113,9 +143,15 @@ const AnearCoreServiceMachineConfig = appId => ({
|
|
|
113
143
|
id: 'initRealtimeMessaging',
|
|
114
144
|
entry: ['initRealtime'],
|
|
115
145
|
on: {
|
|
116
|
-
CONNECTED:
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
CONNECTED: [
|
|
147
|
+
{
|
|
148
|
+
guard: 'acceptLifecycleCommands',
|
|
149
|
+
actions: ['createEventsCreationChannel', 'subscribeCreateEventMessages', ({ context }) => logger.info(`[ACSM] Realtime messaging connected for app ${context.appId}`)]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
actions: ({ context }) => logger.warn(`[ACSM] Realtime connected, but shutdown already requested for app ${context.appId}; not subscribing to CREATE/LOAD commands`)
|
|
153
|
+
}
|
|
154
|
+
],
|
|
119
155
|
ATTACHED: 'uploadNewImageAssets'
|
|
120
156
|
}
|
|
121
157
|
},
|
|
@@ -177,15 +213,18 @@ const AnearCoreServiceMachineConfig = appId => ({
|
|
|
177
213
|
logger.debug(`Waiting on ${shortName} lifecycle command`)
|
|
178
214
|
},
|
|
179
215
|
on: {
|
|
180
|
-
CREATE_EVENT:
|
|
181
|
-
actions: ['startNewEventMachine']
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
CREATE_EVENT: [
|
|
217
|
+
{ guard: 'acceptLifecycleCommands', actions: ['startNewEventMachine'] },
|
|
218
|
+
{ actions: ['logIgnoredLifecycleCommand'] }
|
|
219
|
+
],
|
|
220
|
+
LOAD_EVENT: [
|
|
221
|
+
{ guard: 'acceptLifecycleCommands', actions: ['startNewEventMachine'] },
|
|
222
|
+
{ actions: ['logIgnoredLifecycleCommand'] }
|
|
223
|
+
],
|
|
186
224
|
EVENT_MACHINE_EXIT: {
|
|
187
225
|
actions: [
|
|
188
|
-
'cleanupEventMachine'
|
|
226
|
+
'cleanupEventMachine',
|
|
227
|
+
'exitIfNoActiveEvents'
|
|
189
228
|
]
|
|
190
229
|
}
|
|
191
230
|
}
|
|
@@ -275,7 +314,82 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
275
314
|
self,
|
|
276
315
|
'LOAD_EVENT'
|
|
277
316
|
)
|
|
278
|
-
logger.info(`[ACSM] Subscribed to CREATE_EVENT and LOAD_EVENT on events channel`)
|
|
317
|
+
logger.info(`[ACSM] Subscribed to CREATE_EVENT and LOAD_EVENT on events channel (pid=${process.pid})`)
|
|
318
|
+
},
|
|
319
|
+
logIgnoredLifecycleCommand: ({ event, context }) => {
|
|
320
|
+
logger.warn(`[ACSM] Ignoring ${event.type} because shutdown has been requested (appId=${context.appId} pid=${process.pid})`)
|
|
321
|
+
},
|
|
322
|
+
logSigtermReceived: ({ context, event }) => {
|
|
323
|
+
const count = event?.data?.count || 1
|
|
324
|
+
const activeIds = Object.keys(context.anearEventMachines || {})
|
|
325
|
+
const activeCount = activeIds.length
|
|
326
|
+
|
|
327
|
+
if (count === 1) {
|
|
328
|
+
if (activeCount === 0) {
|
|
329
|
+
logger.info(`[ACSM] SIGTERM received: detaching create/load channel and exiting cleanly (no active events) (pid=${process.pid})`)
|
|
330
|
+
} else {
|
|
331
|
+
logger.info(`[ACSM] SIGTERM received: detaching create/load channel; stopping all CREATE_EVENT/LOAD_EVENT requests; waiting for ${activeCount} active event(s) to terminate (pid=${process.pid})`)
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
logger.warn(`[ACSM] SIGTERM received again (count=${count}) while shutdown in progress (pid=${process.pid})`)
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
hardExitIfSecondSigterm: ({ context, event }) => {
|
|
338
|
+
const count = event?.data?.count || 1
|
|
339
|
+
if (context.shutdownRequested && count >= 2) {
|
|
340
|
+
const activeIds = Object.keys(context.anearEventMachines || {})
|
|
341
|
+
const activeCount = activeIds.length
|
|
342
|
+
if (activeCount > 0) {
|
|
343
|
+
logger.warn(`[ACSM] Second SIGTERM received; forcing shutdown with ${activeCount} active event(s) still running (pid=${process.pid})`)
|
|
344
|
+
} else {
|
|
345
|
+
logger.warn(`[ACSM] Second SIGTERM received; forcing shutdown (pid=${process.pid})`)
|
|
346
|
+
}
|
|
347
|
+
// Hard exit: do not wait for cleanup.
|
|
348
|
+
process.exit(1)
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
markShutdownRequested: assign({
|
|
352
|
+
shutdownRequested: () => true
|
|
353
|
+
}),
|
|
354
|
+
detachCreateEventChannel: ({ context }) => {
|
|
355
|
+
const ch = context.newEventCreationChannel
|
|
356
|
+
if (!ch) {
|
|
357
|
+
logger.info(`[ACSM] SIGTERM received before create-event channel existed; nothing to detach yet (pid=${process.pid})`)
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
// Unsubscribe message listeners to prevent CREATE/LOAD callbacks firing.
|
|
363
|
+
if (typeof ch.unsubscribe === 'function') {
|
|
364
|
+
ch.unsubscribe('CREATE_EVENT')
|
|
365
|
+
ch.unsubscribe('LOAD_EVENT')
|
|
366
|
+
}
|
|
367
|
+
} catch (e) {
|
|
368
|
+
logger.warn('[ACSM] Error unsubscribing create-event channel listeners', e)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
// Detach to stop receiving messages from Ably for this channel.
|
|
373
|
+
const maybePromise = ch.detach()
|
|
374
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
375
|
+
maybePromise.catch(e => logger.warn('[ACSM] Error detaching create-event channel', e))
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
logger.warn('[ACSM] Error detaching create-event channel', e)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
logger.info(`[ACSM] Detached from create-event channel; will not create/load any more events (pid=${process.pid})`)
|
|
382
|
+
},
|
|
383
|
+
exitIfNoActiveEvents: ({ context }) => {
|
|
384
|
+
if (!context.shutdownRequested) return
|
|
385
|
+
|
|
386
|
+
const activeCount = Object.keys(context.anearEventMachines || {}).length
|
|
387
|
+
if (activeCount === 0) {
|
|
388
|
+
scheduleExit(
|
|
389
|
+
0,
|
|
390
|
+
'[ACSM] SIGTERM shutdown complete: no active events remaining; exiting cleanly'
|
|
391
|
+
)
|
|
392
|
+
}
|
|
279
393
|
},
|
|
280
394
|
startNewEventMachine: assign(
|
|
281
395
|
{
|
|
@@ -321,7 +435,8 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
321
435
|
retry_with_backoff_delay: ({ context }) => context.retryDelay
|
|
322
436
|
},
|
|
323
437
|
guards: {
|
|
324
|
-
noImageAssetFilesFound: ({ event }) => event.output === null
|
|
438
|
+
noImageAssetFilesFound: ({ event }) => event.output === null,
|
|
439
|
+
acceptLifecycleCommands: ({ context }) => !context.shutdownRequested
|
|
325
440
|
}
|
|
326
441
|
}
|
|
327
442
|
|
|
@@ -337,7 +452,26 @@ const AnearCoreServiceMachine = (appEventMachineFactory, appParticipantMachineFa
|
|
|
337
452
|
|
|
338
453
|
const coreServiceMachine = createMachine(machineConfig, AnearCoreServiceMachineFunctions)
|
|
339
454
|
|
|
340
|
-
|
|
455
|
+
const actor = createActor(coreServiceMachine, { input: anearCoreServiceMachineContext }).start()
|
|
456
|
+
|
|
457
|
+
if (!_sigtermHandlerInstalled) {
|
|
458
|
+
_sigtermHandlerInstalled = true
|
|
459
|
+
let sigtermCount = 0
|
|
460
|
+
process.on('SIGTERM', () => {
|
|
461
|
+
sigtermCount += 1
|
|
462
|
+
try {
|
|
463
|
+
// Log at the point we definitely caught the signal (even if the actor is wedged).
|
|
464
|
+
// This should land in the same log file as ACSM startup logs.
|
|
465
|
+
logger.info(`[ACSM] process SIGTERM caught (count=${sigtermCount}) pid=${process.pid}`)
|
|
466
|
+
actor.send({ type: 'SIGTERM', data: { count: sigtermCount } })
|
|
467
|
+
} catch (e) {
|
|
468
|
+
// If actor is gone or send fails, fall back to hard exit on second signal.
|
|
469
|
+
if (sigtermCount >= 2) process.exit(1)
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return actor
|
|
341
475
|
}
|
|
342
476
|
|
|
343
477
|
module.exports = AnearCoreServiceMachine
|
|
@@ -42,12 +42,14 @@ class AssetFileCollector {
|
|
|
42
42
|
await this.walkDirectory(fullPath, filesData);
|
|
43
43
|
} else if (entry.isFile()) {
|
|
44
44
|
const relativePath = path.relative(this.baseDir, fullPath).replace(/\\/g, '/'); // Ensure forward slashes
|
|
45
|
+
const stat = await fs.stat(fullPath)
|
|
45
46
|
const contentHash = await this.computeFileHash(fullPath);
|
|
46
47
|
const contentType = this.getContentType(fullPath);
|
|
47
48
|
filesData.push({
|
|
48
49
|
path: relativePath,
|
|
49
50
|
content_hash: contentHash,
|
|
50
51
|
content_type: contentType,
|
|
52
|
+
mtime_ms: stat.mtimeMs,
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -130,7 +130,7 @@ class RealtimeMessaging {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
getChannel(channelName, actor, channelParams = {}) {
|
|
133
|
-
logger.debug(`[RTM] creating channel ${channelName} for ${actor.id}`)
|
|
133
|
+
logger.debug(`[RTM] creating channel ${channelName} for ${actor.id} pid=${process.pid}`)
|
|
134
134
|
|
|
135
135
|
const channel = this.ablyRealtime.channels.get(channelName, channelParams)
|
|
136
136
|
this.enableCallbacks(channel, actor)
|
|
@@ -198,7 +198,7 @@ class RealtimeMessaging {
|
|
|
198
198
|
const type = data.type ? data.type : 'NONE'
|
|
199
199
|
const channelShortName = getChannelShortName(channel.name)
|
|
200
200
|
const channelLabel = channelShortName === 'actions' ? 'actions:presence' : channelShortName
|
|
201
|
-
logger.info(`[RTM] received presence ${eventName} participantId=${data.id} on channel ${channelLabel}`)
|
|
201
|
+
logger.info(`[RTM] received presence ${eventName} participantId=${data.id} on channel ${channelLabel} pid=${process.pid} actor=${actor.id}`)
|
|
202
202
|
actor.send({ type: eventName, data })
|
|
203
203
|
}
|
|
204
204
|
)
|
|
@@ -269,7 +269,7 @@ class RealtimeMessaging {
|
|
|
269
269
|
const participantId = message.data?.participantId || null
|
|
270
270
|
const participantIdStr = participantId ? ` participantId=${participantId}` : ''
|
|
271
271
|
const channelShortName = getChannelShortName(channel.name)
|
|
272
|
-
logger.info(`[RTM] received message ${message.name}${participantIdStr} on channel ${channelShortName}`)
|
|
272
|
+
logger.info(`[RTM] received message ${message.name}${participantIdStr} on channel ${channelShortName} pid=${process.pid} actor=${actor.id}`)
|
|
273
273
|
actor.send({ type: message.name, data: message.data })
|
|
274
274
|
}
|
|
275
275
|
)
|