anear-js-api 1.8.0 → 2.0.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.
package/README.md CHANGED
@@ -115,6 +115,31 @@ ANEARAPP_API_VERSION=v1
115
115
  ANEARAPP_API_URL=http://api.lvh.me:3001/developer # Optional, for local development
116
116
  ```
117
117
 
118
+ ### Local Dev Quickstart (no Docker)
119
+
120
+ To keep development behavior close to today:
121
+
122
+ 1. Keep runtime asset publishing enabled (default):
123
+
124
+ ```bash
125
+ unset ANEARAPP_SKIP_RUNTIME_ASSET_SYNC
126
+ ```
127
+
128
+ 2. Start your app process normally:
129
+
130
+ ```bash
131
+ npm install
132
+ node index.js
133
+ ```
134
+
135
+ 3. If your ANAPI environment is not Rails `development`, set:
136
+
137
+ ```bash
138
+ export ANEARAPP_SINGLE_LATEST_VERSION=true
139
+ ```
140
+
141
+ This preserves a single mutable `latest` app version for iterative development.
142
+
118
143
  ### Developer API Authentication
119
144
 
120
145
  `anear-js-api` communicates with the **ANAPI Developer API** (`/developer/v1/*` endpoints) using JWT authentication:
@@ -263,6 +288,29 @@ The JSAPI automatically handles asset management:
263
288
  2. **Image Upload**: All images in `assets/images/` are uploaded to CloudFront
264
289
  3. **Template Preloading**: All PUG templates are preloaded and cached
265
290
 
291
+ ### CI-Managed Assets Mode
292
+
293
+ For containerized deployments, you can publish assets during CI and skip runtime asset sync:
294
+
295
+ ```bash
296
+ ANEARAPP_SKIP_RUNTIME_ASSET_SYNC=true
297
+ ```
298
+
299
+ When this variable is enabled, `AnearCoreServiceMachine` skips image/font/CSS upload states and proceeds directly to template compilation and event lifecycle listening.
300
+
301
+ CI helpers are available under:
302
+
303
+ - `anear-js-api/lib/ci/publishAssets.js`
304
+ - `anear-js-api/lib/ci/registerAppVersion.js`
305
+
306
+ ### Versioned Lifecycle Channels
307
+
308
+ ACSM subscribes to versioned lifecycle channels derived from ANAPI app metadata:
309
+
310
+ `anear:<appId>:v:<appVersion>:e`
311
+
312
+ ANAPI must return `latest_app_version` in developer app payloads for startup channel resolution.
313
+
266
314
  ### Asset Structure
267
315
 
268
316
  ```
@@ -80,6 +80,24 @@ class AnearApi extends ApiService {
80
80
  return attrs
81
81
  }
82
82
 
83
+ async createAppVersion(appId, attrs) {
84
+ logger.debug(`API: POST app_versions for app ${appId}`)
85
+ const json = await this.post("app_versions", attrs, { app: appId })
86
+ return json.data
87
+ }
88
+
89
+ async updateAppVersion(appVersionId, attrs) {
90
+ logger.debug(`API: PUT app_versions/${appVersionId}`)
91
+ const json = await this.put("app_versions", appVersionId, attrs)
92
+ return json.data
93
+ }
94
+
95
+ async getLatestAppVersion(appId) {
96
+ logger.debug(`API: GET apps/${appId}/latest_app_version`)
97
+ const json = await this.get(`apps/${appId}/latest_app_version`)
98
+ return json.data
99
+ }
100
+
83
101
  async saveAppEventContext(eventId, appmContext) {
84
102
  logger.debug(`API: POST developer events/${eventId}/app_event_context`)
85
103
  const path = `events/${eventId}/app_event_context`
@@ -0,0 +1,35 @@
1
+ "use strict"
2
+
3
+ const C = require('../utils/Constants')
4
+ const ImageAssetsUploader = require('../utils/ImageAssetsUploader')
5
+ const FontAssetsUploader = require('../utils/FontAssetsUploader')
6
+ const CssUploader = require('../utils/CssUploader')
7
+
8
+ async function publishAssetsFromCi(appId = process.env.ANEARAPP_APP_ID) {
9
+ if (!appId) {
10
+ throw new Error('ANEARAPP_APP_ID must be set to publish assets from CI')
11
+ }
12
+
13
+ const imageAssetsUploader = new ImageAssetsUploader(C.ImagesDirPath, appId)
14
+ const imageAssetsUrl = await imageAssetsUploader.uploadAssets()
15
+
16
+ const fontAssetsUploader = new FontAssetsUploader(C.FontsDirPath, appId)
17
+ const fontAssetsUrl = await fontAssetsUploader.uploadAssets()
18
+
19
+ const cssUploader = new CssUploader(
20
+ C.CssDirPath,
21
+ imageAssetsUrl,
22
+ fontAssetsUrl,
23
+ appId
24
+ )
25
+ const cssUrl = await cssUploader.uploadCss()
26
+
27
+ return {
28
+ appId,
29
+ imageAssetsUrl,
30
+ fontAssetsUrl,
31
+ cssUrl
32
+ }
33
+ }
34
+
35
+ module.exports = { publishAssetsFromCi }
@@ -0,0 +1,43 @@
1
+ "use strict"
2
+
3
+ const AnearApi = require('../api/AnearApi')
4
+
5
+ async function registerAppVersionFromCi({
6
+ appId = process.env.ANEARAPP_APP_ID,
7
+ version = process.env.APP_VERSION,
8
+ gitSha = process.env.GITHUB_SHA,
9
+ imageUri = process.env.IMAGE_URI,
10
+ assetManifestHash = process.env.ASSET_MANIFEST_HASH,
11
+ status = 'building'
12
+ } = {}) {
13
+ if (!appId || !version) {
14
+ throw new Error('ANEARAPP_APP_ID and APP_VERSION are required to register an app version')
15
+ }
16
+
17
+ const data = await AnearApi.createAppVersion(appId, {
18
+ version,
19
+ status,
20
+ git_sha: gitSha,
21
+ image_uri: imageUri,
22
+ asset_manifest_hash: assetManifestHash
23
+ })
24
+
25
+ return data
26
+ }
27
+
28
+ async function markAppVersionStatus(appVersionId, status, extraAttrs = {}) {
29
+ if (!appVersionId || !status) {
30
+ throw new Error('appVersionId and status are required to update app version status')
31
+ }
32
+
33
+ const data = await AnearApi.updateAppVersion(appVersionId, {
34
+ status,
35
+ ...extraAttrs
36
+ })
37
+ return data
38
+ }
39
+
40
+ module.exports = {
41
+ registerAppVersionFromCi,
42
+ markAppVersionStatus
43
+ }
@@ -58,8 +58,9 @@ const ImageAssetsUploader = require('../utils/ImageAssetsUploader')
58
58
  const FontAssetsUploader = require('../utils/FontAssetsUploader')
59
59
  const C = require('../utils/Constants')
60
60
 
61
- const CreateEventChannelNameTemplate = appId => `anear:${appId}:e`
61
+ const CreateVersionedEventChannelNameTemplate = (appId, appVersion) => `anear:${appId}:v:${normalizeVersionToken(appVersion)}:e`
62
62
  const DefaultTemplatesRootDir = "./views"
63
+ const SkipRuntimeAssetSync = process.env.ANEARAPP_SKIP_RUNTIME_ASSET_SYNC === 'true'
63
64
 
64
65
  // Process-wide signal handling.
65
66
  // We install exactly once per process to avoid duplicate handlers in reconnect/restart scenarios.
@@ -152,9 +153,21 @@ const AnearCoreServiceMachineConfig = appId => ({
152
153
  actions: ({ context }) => logger.warn(`[ACSM] Realtime connected, but shutdown already requested for app ${context.appId}; not subscribing to CREATE/LOAD commands`)
153
154
  }
154
155
  ],
155
- ATTACHED: 'uploadNewImageAssets'
156
+ ATTACHED: 'syncAssetsOrSkip'
156
157
  }
157
158
  },
159
+ syncAssetsOrSkip: {
160
+ always: [
161
+ {
162
+ guard: 'runtimeAssetSyncDisabled',
163
+ actions: () => logger.info('[ACSM] Runtime asset sync disabled; assuming CI has already published assets'),
164
+ target: 'loadAndCompilePugTemplates'
165
+ },
166
+ {
167
+ target: 'uploadNewImageAssets'
168
+ }
169
+ ]
170
+ },
158
171
  uploadNewImageAssets: {
159
172
  invoke: {
160
173
  src: 'uploadNewImageAssets',
@@ -298,7 +311,13 @@ const AnearCoreServiceMachineFunctions = {
298
311
  createEventsCreationChannel: assign(
299
312
  {
300
313
  newEventCreationChannel: ({ context, self }) => {
301
- const channelName = CreateEventChannelNameTemplate(context.appId)
314
+ const attrs = context.appData?.data?.attributes || {}
315
+ const appVersion = attrs['latest-app-version']
316
+ if (!appVersion) {
317
+ throw new Error('[ACSM] Missing latest-app-version in app data; cannot subscribe to lifecycle channel')
318
+ }
319
+ const channelName = CreateVersionedEventChannelNameTemplate(context.appId, appVersion)
320
+ logger.info(`[ACSM] Subscribing to versioned lifecycle channel ${channelName}`)
302
321
  return RealtimeMessaging.getChannel(channelName, self)
303
322
  }
304
323
  }
@@ -314,7 +333,7 @@ const AnearCoreServiceMachineFunctions = {
314
333
  self,
315
334
  'LOAD_EVENT'
316
335
  )
317
- logger.info(`[ACSM] Subscribed to CREATE_EVENT and LOAD_EVENT on events channel (pid=${process.pid})`)
336
+ logger.info(`[ACSM] Subscribed to CREATE_EVENT and LOAD_EVENT on versioned lifecycle channel (pid=${process.pid})`)
318
337
  },
319
338
  logIgnoredLifecycleCommand: ({ event, context }) => {
320
339
  logger.warn(`[ACSM] Ignoring ${event.type} because shutdown has been requested (appId=${context.appId} pid=${process.pid})`)
@@ -436,10 +455,15 @@ const AnearCoreServiceMachineFunctions = {
436
455
  },
437
456
  guards: {
438
457
  noImageAssetFilesFound: ({ event }) => event.output === null,
439
- acceptLifecycleCommands: ({ context }) => !context.shutdownRequested
458
+ acceptLifecycleCommands: ({ context }) => !context.shutdownRequested,
459
+ runtimeAssetSyncDisabled: () => SkipRuntimeAssetSync
440
460
  }
441
461
  }
442
462
 
463
+ function normalizeVersionToken(value) {
464
+ return String(value).replace(/[^A-Za-z0-9_.-]/g, '_')
465
+ }
466
+
443
467
  const AnearCoreServiceMachine = (appEventMachineFactory, appParticipantMachineFactory = null) => {
444
468
  const appId = process.env.ANEARAPP_APP_ID
445
469
  const machineConfig = AnearCoreServiceMachineConfig(appId)
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "1.8.0",
3
+ "version": "2.0.1",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
7
- "test": "jest"
7
+ "test": "jest",
8
+ "audit:prod": "npm audit --omit=dev",
9
+ "audit:prod:fix": "npm audit fix --omit=dev",
10
+ "audit:full": "npm audit",
11
+ "audit:full:fix": "npm audit fix"
8
12
  },
9
13
  "repository": {
10
14
  "type": "git",