anear-js-api 1.8.0 → 2.1.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 +48 -0
- package/lib/api/AnearApi.js +18 -0
- package/lib/ci/publishAssets.js +35 -0
- package/lib/ci/registerAppVersion.js +49 -0
- package/lib/state_machines/AnearCoreServiceMachine.js +58 -5
- package/package.json +6 -2
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
|
```
|
package/lib/api/AnearApi.js
CHANGED
|
@@ -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,49 @@
|
|
|
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
|
+
imageAssetsUrl = process.env.IMAGE_ASSETS_URL,
|
|
12
|
+
fontAssetsUrl = process.env.FONT_ASSETS_URL,
|
|
13
|
+
cssUrl = process.env.CSS_URL,
|
|
14
|
+
status = 'building'
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (!appId || !version) {
|
|
17
|
+
throw new Error('ANEARAPP_APP_ID and APP_VERSION are required to register an app version')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const data = await AnearApi.createAppVersion(appId, {
|
|
21
|
+
version,
|
|
22
|
+
status,
|
|
23
|
+
git_sha: gitSha,
|
|
24
|
+
image_uri: imageUri,
|
|
25
|
+
asset_manifest_hash: assetManifestHash,
|
|
26
|
+
image_assets_url: imageAssetsUrl,
|
|
27
|
+
font_assets_url: fontAssetsUrl,
|
|
28
|
+
css_url: cssUrl
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return data
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function markAppVersionStatus(appVersionId, status, extraAttrs = {}) {
|
|
35
|
+
if (!appVersionId || !status) {
|
|
36
|
+
throw new Error('appVersionId and status are required to update app version status')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = await AnearApi.updateAppVersion(appVersionId, {
|
|
40
|
+
status,
|
|
41
|
+
...extraAttrs
|
|
42
|
+
})
|
|
43
|
+
return data
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
registerAppVersionFromCi,
|
|
48
|
+
markAppVersionStatus
|
|
49
|
+
}
|
|
@@ -58,8 +58,11 @@ const ImageAssetsUploader = require('../utils/ImageAssetsUploader')
|
|
|
58
58
|
const FontAssetsUploader = require('../utils/FontAssetsUploader')
|
|
59
59
|
const C = require('../utils/Constants')
|
|
60
60
|
|
|
61
|
-
const
|
|
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'
|
|
64
|
+
const FallbackImageAssetsUrl = process.env.ANEARAPP_IMAGE_ASSETS_URL || null
|
|
65
|
+
const FallbackFontAssetsUrl = process.env.ANEARAPP_FONT_ASSETS_URL || null
|
|
63
66
|
|
|
64
67
|
// Process-wide signal handling.
|
|
65
68
|
// We install exactly once per process to avoid duplicate handlers in reconnect/restart scenarios.
|
|
@@ -152,7 +155,33 @@ const AnearCoreServiceMachineConfig = appId => ({
|
|
|
152
155
|
actions: ({ context }) => logger.warn(`[ACSM] Realtime connected, but shutdown already requested for app ${context.appId}; not subscribing to CREATE/LOAD commands`)
|
|
153
156
|
}
|
|
154
157
|
],
|
|
155
|
-
ATTACHED: '
|
|
158
|
+
ATTACHED: 'syncAssetsOrSkip'
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
syncAssetsOrSkip: {
|
|
162
|
+
always: [
|
|
163
|
+
{
|
|
164
|
+
guard: 'runtimeAssetSyncDisabled',
|
|
165
|
+
actions: () => logger.info('[ACSM] Runtime asset sync disabled; assuming CI has already published assets'),
|
|
166
|
+
target: 'resolveVersionAssetUrls'
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
target: 'uploadNewImageAssets'
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
resolveVersionAssetUrls: {
|
|
174
|
+
invoke: {
|
|
175
|
+
src: 'fetchLatestAppVersionAssets',
|
|
176
|
+
input: ({ context }) => ({ appId: context.appId }),
|
|
177
|
+
onDone: {
|
|
178
|
+
actions: assign({
|
|
179
|
+
imageAssetsUrl: ({ event }) => event.output.imageAssetsUrl,
|
|
180
|
+
fontAssetsUrl: ({ event }) => event.output.fontAssetsUrl
|
|
181
|
+
}),
|
|
182
|
+
target: 'loadAndCompilePugTemplates'
|
|
183
|
+
},
|
|
184
|
+
onError: '#failure'
|
|
156
185
|
}
|
|
157
186
|
},
|
|
158
187
|
uploadNewImageAssets: {
|
|
@@ -242,6 +271,19 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
242
271
|
// v5 pattern: async "services" become Promise Actors via `fromPromise`.
|
|
243
272
|
// The invoked actor receives `input` from the invoking state's `invoke.input`.
|
|
244
273
|
fetchAppData: fromPromise(({ input }) => AnearApi.getApp(input.appId)),
|
|
274
|
+
fetchLatestAppVersionAssets: fromPromise(async ({ input }) => {
|
|
275
|
+
const latestVersion = await AnearApi.getLatestAppVersion(input.appId)
|
|
276
|
+
const attrs = latestVersion?.attributes || {}
|
|
277
|
+
const imageAssetsUrl = attrs['image-assets-url'] || FallbackImageAssetsUrl
|
|
278
|
+
const fontAssetsUrl = attrs['font-assets-url'] || FallbackFontAssetsUrl
|
|
279
|
+
|
|
280
|
+
if (!imageAssetsUrl) {
|
|
281
|
+
throw new Error('[ACSM] Missing image-assets-url for latest app version; set app_version image_assets_url in ANAPI or ANEARAPP_IMAGE_ASSETS_URL env var')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
logger.info(`[ACSM] Using pre-published version assets image_base=${imageAssetsUrl}`)
|
|
285
|
+
return { imageAssetsUrl, fontAssetsUrl }
|
|
286
|
+
}),
|
|
245
287
|
uploadNewImageAssets: fromPromise(({ input }) => {
|
|
246
288
|
const uploader = new ImageAssetsUploader(C.ImagesDirPath, input.appId)
|
|
247
289
|
return uploader.uploadAssets()
|
|
@@ -298,7 +340,13 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
298
340
|
createEventsCreationChannel: assign(
|
|
299
341
|
{
|
|
300
342
|
newEventCreationChannel: ({ context, self }) => {
|
|
301
|
-
const
|
|
343
|
+
const attrs = context.appData?.data?.attributes || {}
|
|
344
|
+
const appVersion = attrs['latest-app-version']
|
|
345
|
+
if (!appVersion) {
|
|
346
|
+
throw new Error('[ACSM] Missing latest-app-version in app data; cannot subscribe to lifecycle channel')
|
|
347
|
+
}
|
|
348
|
+
const channelName = CreateVersionedEventChannelNameTemplate(context.appId, appVersion)
|
|
349
|
+
logger.info(`[ACSM] Subscribing to versioned lifecycle channel ${channelName}`)
|
|
302
350
|
return RealtimeMessaging.getChannel(channelName, self)
|
|
303
351
|
}
|
|
304
352
|
}
|
|
@@ -314,7 +362,7 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
314
362
|
self,
|
|
315
363
|
'LOAD_EVENT'
|
|
316
364
|
)
|
|
317
|
-
logger.info(`[ACSM] Subscribed to CREATE_EVENT and LOAD_EVENT on
|
|
365
|
+
logger.info(`[ACSM] Subscribed to CREATE_EVENT and LOAD_EVENT on versioned lifecycle channel (pid=${process.pid})`)
|
|
318
366
|
},
|
|
319
367
|
logIgnoredLifecycleCommand: ({ event, context }) => {
|
|
320
368
|
logger.warn(`[ACSM] Ignoring ${event.type} because shutdown has been requested (appId=${context.appId} pid=${process.pid})`)
|
|
@@ -436,10 +484,15 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
436
484
|
},
|
|
437
485
|
guards: {
|
|
438
486
|
noImageAssetFilesFound: ({ event }) => event.output === null,
|
|
439
|
-
acceptLifecycleCommands: ({ context }) => !context.shutdownRequested
|
|
487
|
+
acceptLifecycleCommands: ({ context }) => !context.shutdownRequested,
|
|
488
|
+
runtimeAssetSyncDisabled: () => SkipRuntimeAssetSync
|
|
440
489
|
}
|
|
441
490
|
}
|
|
442
491
|
|
|
492
|
+
function normalizeVersionToken(value) {
|
|
493
|
+
return String(value).replace(/[^A-Za-z0-9_.-]/g, '_')
|
|
494
|
+
}
|
|
495
|
+
|
|
443
496
|
const AnearCoreServiceMachine = (appEventMachineFactory, appParticipantMachineFactory = null) => {
|
|
444
497
|
const appId = process.env.ANEARAPP_APP_ID
|
|
445
498
|
const machineConfig = AnearCoreServiceMachineConfig(appId)
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anear-js-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.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",
|