arcane-os 0.13.0 → 0.13.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.1
4
+
5
+ - Refresh live development app membership from the current authored descriptor
6
+ or package-only configuration. New explicit includes, exclusions, entry paths,
7
+ and PWA settings take effect on the next relevant request without a server
8
+ restart or writes to consumer projections.
9
+ - Generate PWA metadata and offline inventories from the same app snapshot.
10
+ Coalesce concurrent metadata reads and inventory work within that snapshot,
11
+ and keep older inventory completion from replacing newer metadata.
12
+
3
13
  ## 0.13.0
4
14
 
5
15
  - Add the reusable `pwa-install.html` component and shared browser installation
@@ -116,6 +116,15 @@ development-refresh lock, then releases the lock before binding the listener.
116
116
  The authored descriptor remains unchanged, and package-only apps retain their
117
117
  existing path. An enabled PWA receives generated manifests directly from this
118
118
  source server without creating `dist` output.
119
+ While serving, the SDK rereads changed metadata for the selected app before
120
+ application, root-navigation, and generated-PWA requests. It projects the current
121
+ authored descriptor in memory, or reads the current package configuration for a
122
+ package-only app, without writing either file. Include/exclude rules, entry
123
+ selection, and PWA settings share one request snapshot. Overlapping reads reuse
124
+ one metadata task; unchanged files reuse their parsed selection. Shared runtime
125
+ requests do not wait for that refresh. Full offline inventories are enumerated
126
+ only for worker/offline-manifest requests, with separate state for each selected
127
+ app snapshot so an older inventory cannot overwrite newer generated metadata.
119
128
  Changed resource requests return the complete current saved source without
120
129
  packaging, copying files into `dist`, or restarting the server. Conditional
121
130
  requests for unchanged resources return `304`. Enabled PWAs check on page load
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -6,6 +6,8 @@ import https from 'node:https';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import {resolveWorkspace} from './workspace.mjs';
9
+ import {APP_DESCRIPTOR_NAME, projectPackageManifest} from './app-descriptor.mjs';
10
+ import {APP_CONFIG_NAME, validateAppConfig} from './packager/core.mjs';
9
11
  import {createEventQueue} from './event-queue.mjs';
10
12
  import {
11
13
  applyPwaEntryReferences,inspectImportMapHtml,MANAGED_IMPORT_MAP_ATTRIBUTE,
@@ -324,13 +326,15 @@ async function openSafeFile(root, segments, {readContent = true} = {}) {
324
326
  async function serveGeneratedRepresentation(fileServer, request, response, {
325
327
  lastModified, contentType, body
326
328
  }) {
327
- const modified = lastModified.toUTCString();
328
- response.setHeader('Last-Modified', modified);
329
- const requested = Date.parse(request.headers['if-modified-since']);
330
- if (Number.isFinite(requested) && Date.parse(modified) <= requested) {
331
- response.writeHead(304);
332
- response.end();
333
- return;
329
+ if (lastModified) {
330
+ const modified = lastModified.toUTCString();
331
+ response.setHeader('Last-Modified', modified);
332
+ const requested = Date.parse(request.headers['if-modified-since']);
333
+ if (Number.isFinite(requested) && Date.parse(modified) <= requested) {
334
+ response.writeHead(304);
335
+ response.end();
336
+ return;
337
+ }
334
338
  }
335
339
  response.setHeader('Content-Type', contentType);
336
340
  await fileServer.serve(request, response, await body());
@@ -459,6 +463,7 @@ async function sourceRoutes(workspaceRoot,appId,{
459
463
  return {
460
464
  workspaceRoot:resolved.workspaceRoot,
461
465
  workspaceMode:resolved.config.workspaceMode,
466
+ config:resolved.config,
462
467
  appId:resolved.appId,
463
468
  app:resolved.app.manifest,
464
469
  startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
@@ -470,6 +475,7 @@ async function sourceRoutes(workspaceRoot,appId,{
470
475
  return {
471
476
  workspaceRoot:resolved.workspaceRoot,
472
477
  workspaceMode:'integrated',
478
+ config:resolved.config,
473
479
  appId:resolved.appId,
474
480
  app:resolved.app.manifest,
475
481
  startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
@@ -488,6 +494,7 @@ async function sourceRoutes(workspaceRoot,appId,{
488
494
  return {
489
495
  workspaceRoot:resolved.workspaceRoot,
490
496
  workspaceMode:'external',
497
+ config:resolved.config,
491
498
  appId:resolved.appId,
492
499
  app:resolved.app.manifest,
493
500
  startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
@@ -840,7 +847,6 @@ async function startOwnedDevServer({
840
847
  })
841
848
  :await packagedRoutes(releaseRoot);
842
849
  const mappings=deterministicMappings(routeSet.mappings);
843
- const pwaEnabled = mode === 'source' ? routeSet.app?.pwa?.enabled === true : routeSet.pwa;
844
850
  const versionPath = mode === 'source'
845
851
  ? sdkRuntimeSourceRoot === undefined
846
852
  ? path.join(routeSet.workspaceRoot, 'arcane.lock.json')
@@ -928,24 +934,89 @@ async function startOwnedDevServer({
928
934
  if(info.isSymbolicLink()||!info.isDirectory())fail(`Server route root must be a real directory: ${mapping.root}.`);
929
935
  mapping.root=await realpath(mapping.root);
930
936
  }
931
- let pwaArtifacts;
932
- let pwaInventoryTask;
933
- const generatedMetadata = new Map();
937
+ let currentSourceRoutes = {...routeSet, mappings};
938
+ let sourceManifestInput;
939
+ let sourceManifestTask;
940
+ const appMapping = mappings.find(
941
+ function selectedApplicationMapping(mapping) {
942
+ return mapping.prefix[0] === 'apps' && mapping.prefix[1] === routeSet.appId;
943
+ }
944
+ );
945
+ async function refreshSourceRoutes() {
946
+ // Share concurrent metadata reads; unchanged descriptors need no parse
947
+ // or projection, and unrelated runtime requests do not wait here.
948
+ if (!sourceManifestTask) {
949
+ sourceManifestTask = readCurrentSourceRoutes().finally(
950
+ function releaseSourceManifestTask() {
951
+ sourceManifestTask = null;
952
+ }
953
+ );
954
+ }
955
+ return sourceManifestTask;
956
+ }
957
+ async function readCurrentSourceRoutes() {
958
+ throwIfAborted(signal);
959
+ let manifestPath = path.join(appMapping.root, APP_DESCRIPTOR_NAME);
960
+ let authored = true;
961
+ let input;
962
+ try {
963
+ input = await lstat(manifestPath);
964
+ } catch (error) {
965
+ if (error.code !== 'ENOENT') throw error;
966
+ authored = false;
967
+ manifestPath = path.join(appMapping.root, APP_CONFIG_NAME);
968
+ input = await lstat(manifestPath);
969
+ }
970
+ if (sourceManifestInput?.path === manifestPath
971
+ && sourceManifestInput.modifiedAt === input.mtimeMs) {
972
+ return currentSourceRoutes;
973
+ }
974
+ const value = JSON.parse(await readFile(manifestPath, 'utf8'));
975
+ const manifest = validateAppConfig(
976
+ authored ? projectPackageManifest(value) : value,
977
+ routeSet.appId,
978
+ routeSet.config,
979
+ manifestPath
980
+ );
981
+ const currentAppMapping = {
982
+ ...appMapping,
983
+ include: manifest.include,
984
+ allow: function currentSourcePathAllowed(relative) {
985
+ return sourcePathAllowed(relative, manifest);
986
+ }
987
+ };
988
+ // Each request retains its own selection while an inventory or file read
989
+ // runs. HTTP reads never rewrite the consumer's authored projections.
990
+ currentSourceRoutes = {
991
+ ...routeSet,
992
+ app: manifest,
993
+ startPath: `/apps/${routeSet.appId}/${manifest.entry}`,
994
+ mappings: mappings.map(
995
+ function currentSourceMapping(mapping) {
996
+ return mapping === appMapping ? currentAppMapping : mapping;
997
+ }
998
+ )
999
+ };
1000
+ sourceManifestInput = {path: manifestPath, modifiedAt: input.mtimeMs};
1001
+ return currentSourceRoutes;
1002
+ }
1003
+ const pwaStates = new WeakMap();
1004
+ let currentPwaState;
934
1005
  const pwaResourceUrls = new Set();
935
- function developmentPwaArtifacts(assets = []) {
1006
+ function developmentPwaArtifacts(selectedRoutes, assets = [], version = assetVersion) {
936
1007
  return createPwaArtifacts(
937
1008
  {
938
1009
  app: {
939
1010
  id: routeSet.appId,
940
- displayName: routeSet.app.displayName,
941
- version: routeSet.app.version,
942
- entry: routeSet.startPath
1011
+ displayName: selectedRoutes.app.displayName,
1012
+ version: selectedRoutes.app.version,
1013
+ entry: selectedRoutes.startPath
943
1014
  },
944
- sdkVersion: assetVersion,
945
- pwa: routeSet.app.pwa,
1015
+ sdkVersion: version,
1016
+ pwa: selectedRoutes.app.pwa,
946
1017
  files: [],
947
1018
  assets,
948
- navigationAliases: {'/': routeSet.startPath},
1019
+ navigationAliases: {'/': selectedRoutes.startPath},
949
1020
  basePath: '/',
950
1021
  appBase: `/apps/${routeSet.appId}/`,
951
1022
  runtimeBase: '/arcane/sdk/',
@@ -953,46 +1024,66 @@ async function startOwnedDevServer({
953
1024
  }
954
1025
  );
955
1026
  }
956
- function rememberPwaArtifacts(artifacts) {
1027
+ function rememberPwaArtifacts(state, artifacts, generatedAt = new Date()) {
957
1028
  for (const file of artifacts.files) {
958
- const previous = generatedMetadata.get(file.path);
1029
+ const previous = state.metadata.get(file.path);
959
1030
  const lastModified = previous?.content === file.content
960
1031
  ? previous.lastModified
961
- : new Date();
962
- generatedMetadata.set(
1032
+ : generatedAt;
1033
+ state.metadata.set(
963
1034
  file.path,
964
1035
  {content: file.content, lastModified}
965
1036
  );
966
1037
  }
967
- pwaArtifacts = artifacts;
1038
+ state.artifacts = artifacts;
968
1039
  return artifacts;
969
1040
  }
970
- async function sourcePwaArtifact(targetPath) {
1041
+ async function sourcePwaArtifact(targetPath, selectedRoutes) {
1042
+ let state = pwaStates.get(selectedRoutes);
1043
+ if (!state) {
1044
+ state = {metadata: new Map(currentPwaState?.metadata)};
1045
+ pwaStates.set(selectedRoutes, state);
1046
+ if (selectedRoutes === currentSourceRoutes) currentPwaState = state;
1047
+ }
971
1048
  if (targetPath === '/arcane-sw.js'
972
1049
  || targetPath === '/arcane-offline.json') {
973
- if (!pwaInventoryTask) {
974
- pwaInventoryTask = Promise.all(
975
- [sourcePwaAssets(routeSet, mappings, signal, pwaResourceUrls, resourcePaths), selectedAssetVersion()]
1050
+ if (!state.inventoryTask) {
1051
+ // Timestamp selection before traversal, so an older snapshot
1052
+ // finishing later cannot acquire a newer HTTP modification date.
1053
+ const generatedAt = new Date();
1054
+ state.inventoryTask = Promise.all(
1055
+ [
1056
+ sourcePwaAssets(selectedRoutes, selectedRoutes.mappings, signal, pwaResourceUrls, resourcePaths),
1057
+ selectedAssetVersion().then(
1058
+ function rememberCurrentInventoryVersion(version) {
1059
+ if (selectedRoutes === currentSourceRoutes) rememberAssetVersion(version);
1060
+ return version;
1061
+ }
1062
+ )
1063
+ ]
976
1064
  ).then(
977
1065
  function prepareSourceOfflineInventory([assets, version]) {
978
- rememberAssetVersion(version);
979
- return rememberPwaArtifacts(developmentPwaArtifacts(assets));
1066
+ return rememberPwaArtifacts(
1067
+ state,
1068
+ developmentPwaArtifacts(selectedRoutes, assets, version),
1069
+ generatedAt
1070
+ );
980
1071
  }
981
1072
  ).finally(
982
1073
  function releaseSourceInventoryTask() {
983
- pwaInventoryTask = null;
1074
+ state.inventoryTask = null;
984
1075
  }
985
1076
  );
986
1077
  }
987
- await pwaInventoryTask;
1078
+ await state.inventoryTask;
988
1079
  }
989
- const artifacts = pwaArtifacts ?? rememberPwaArtifacts(developmentPwaArtifacts());
1080
+ const artifacts = state.artifacts ?? rememberPwaArtifacts(state, developmentPwaArtifacts(selectedRoutes));
990
1081
  const file = artifacts.files.find(
991
1082
  function requestedPwaFile(file) {
992
1083
  return `/${file.path}` === targetPath;
993
1084
  }
994
1085
  );
995
- return {...file, lastModified: generatedMetadata.get(file.path).lastModified};
1086
+ return {...file, lastModified: state.metadata.get(file.path).lastModified};
996
1087
  }
997
1088
  // Remember actual resource edges as their owners are served, not every
998
1089
  // HTML/JS/CSS file in an application's document or attachment corpus.
@@ -1018,16 +1109,24 @@ async function startOwnedDevServer({
1018
1109
  const target=parseRequestTarget(request.url);
1019
1110
  if(!target){deny(response,400,'Invalid request path.');return;}
1020
1111
  const {segments}=target;
1112
+ const generatedPwaPath = ['/arcane.webmanifest', '/arcane-offline.json', '/arcane-sw.js', '/arcane-pwa.mjs']
1113
+ .includes(target.path);
1114
+ const appRequest = segments[0] === 'apps' && segments[1] === routeSet.appId;
1115
+ const selectedRoutes = mode === 'source' && (appRequest || segments.length === 0 || generatedPwaPath)
1116
+ ? await refreshSourceRoutes() : currentSourceRoutes;
1117
+ const pwaEnabled = mode === 'source' ? selectedRoutes.app?.pwa?.enabled === true : routeSet.pwa;
1021
1118
  if (mode === 'source' && pwaEnabled
1022
- && ['/arcane.webmanifest', '/arcane-offline.json', '/arcane-sw.js', '/arcane-pwa.mjs'].includes(target.path)) {
1023
- const generated = await sourcePwaArtifact(target.path);
1119
+ && generatedPwaPath) {
1120
+ const generated = await sourcePwaArtifact(target.path, selectedRoutes);
1024
1121
  response.setHeader('Cache-Control', 'no-cache');
1025
1122
  await serveGeneratedRepresentation(
1026
1123
  fileServer,
1027
1124
  request,
1028
1125
  response,
1029
1126
  {
1030
- lastModified: generated.lastModified,
1127
+ // An older in-flight snapshot may finish after a newer
1128
+ // selection. Its body cannot validate that newer output.
1129
+ lastModified: selectedRoutes === currentSourceRoutes ? generated.lastModified : undefined,
1031
1130
  contentType: MIME_TYPES.get(path.extname(generated.path)),
1032
1131
  body: function generatedPwaBody() {
1033
1132
  return generated.content;
@@ -1037,12 +1136,18 @@ async function startOwnedDevServer({
1037
1136
  return;
1038
1137
  }
1039
1138
  if(segments.length===0){
1040
- response.writeHead(302,{location:routeSet.startPath});
1139
+ response.writeHead(302,{location:selectedRoutes.startPath});
1041
1140
  response.end();
1042
1141
  return;
1043
1142
  }
1044
- const mapping=mappings.find(route=>
1045
- route.prefix.every((segment,index)=>segments[index]===segment)
1143
+ const mapping = selectedRoutes.mappings.find(
1144
+ function requestedSourceMapping(route) {
1145
+ return route.prefix.every(
1146
+ function matchingSourcePrefix(segment, index) {
1147
+ return segments[index] === segment;
1148
+ }
1149
+ );
1150
+ }
1046
1151
  );
1047
1152
  if(!mapping){deny(response,404,'Not found.');return;}
1048
1153
  const relative=segments.slice(mapping.prefix.length);
@@ -1064,8 +1169,8 @@ async function startOwnedDevServer({
1064
1169
  const html=extension==='.html'||extension==='.htm';
1065
1170
  const selectedPwaDocument = mode === 'source' && pwaEnabled && html
1066
1171
  && mapping.prefix[0] === 'apps' && mapping.prefix[1] === routeSet.appId
1067
- && routeSet.app.include.includes(relative.join('/'));
1068
- const selectedDocument = target.path === routeSet.startPath || selectedPwaDocument;
1172
+ && selectedRoutes.app.include.includes(relative.join('/'));
1173
+ const selectedDocument = target.path === selectedRoutes.startPath || selectedPwaDocument;
1069
1174
  const managedDocument = !selectedDocument && html && await isManagedDocument(opened);
1070
1175
  const entryDocument = selectedDocument || managedDocument;
1071
1176
  const managedMap=path.basename(opened.candidate)==='arcane.importmap.json';