1681-engine 0.5.9 → 0.6.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.
Files changed (210) hide show
  1. package/CHANGELOG.md +92 -1
  2. package/README.md +15 -6
  3. package/dist/1681-engine.js +588 -465
  4. package/dist/ai/behaviorTree.js +198 -0
  5. package/dist/ai/buildTree.js +54 -0
  6. package/dist/ambient/life/Birds.js +73 -0
  7. package/dist/ambient/life/Butterflies.js +43 -0
  8. package/dist/ambient/life/ChimneySmoke.js +50 -0
  9. package/dist/ambient/life/Drift.js +75 -0
  10. package/dist/ambient/life/Fireflies.js +52 -0
  11. package/dist/ambient/life/lifeMotion.js +49 -0
  12. package/dist/assets/AssetLoader.js +115 -70
  13. package/dist/assets/Prepainter.js +61 -0
  14. package/dist/assets/workerPainter.js +39 -0
  15. package/dist/audio/spatial.js +118 -0
  16. package/dist/cjs/electron/electronLike.d.ts +55 -0
  17. package/dist/cjs/electron/electronLike.js +7 -0
  18. package/dist/cjs/electron/index.d.ts +14 -0
  19. package/dist/cjs/electron/index.js +20 -0
  20. package/dist/cjs/electron/lockdown.d.ts +33 -0
  21. package/dist/cjs/electron/lockdown.js +69 -0
  22. package/dist/cjs/electron/lockdownRules.d.ts +28 -0
  23. package/dist/cjs/electron/lockdownRules.js +61 -0
  24. package/dist/cjs/electron/sessionStore.d.ts +40 -0
  25. package/dist/cjs/electron/sessionStore.js +77 -0
  26. package/dist/cjs/electron/shellId.d.ts +22 -0
  27. package/dist/cjs/electron/shellId.js +31 -0
  28. package/dist/cjs/electron/webFiles.d.ts +13 -0
  29. package/dist/cjs/electron/webFiles.js +52 -0
  30. package/dist/cjs/electron/webPatcher.d.ts +83 -0
  31. package/dist/cjs/electron/webPatcher.js +223 -0
  32. package/dist/cjs/electron/webPointers.d.ts +19 -0
  33. package/dist/cjs/electron/webPointers.js +35 -0
  34. package/dist/cjs/package.json +1 -0
  35. package/dist/cjs/updates/index.d.ts +8 -0
  36. package/dist/cjs/updates/index.js +21 -0
  37. package/dist/cjs/updates/manifest.d.ts +73 -0
  38. package/dist/cjs/updates/manifest.js +67 -0
  39. package/dist/cjs/updates/pickUpdate.d.ts +42 -0
  40. package/dist/cjs/updates/pickUpdate.js +52 -0
  41. package/dist/cjs/updates/webPlan.d.ts +20 -0
  42. package/dist/cjs/updates/webPlan.js +34 -0
  43. package/dist/dialogue/DialogueRunner.js +173 -0
  44. package/dist/dialogue/expr.js +137 -0
  45. package/dist/dialogue/validateDialogue.js +52 -0
  46. package/dist/ecs/prefab.js +94 -0
  47. package/dist/electron/lockdown.js +31 -0
  48. package/dist/electron/lockdownRules.js +42 -0
  49. package/dist/electron/sessionStore.js +48 -0
  50. package/dist/electron/shellId.js +18 -0
  51. package/dist/electron/webFiles.js +36 -0
  52. package/dist/electron/webPatcher.js +153 -0
  53. package/dist/electron/webPointers.js +25 -0
  54. package/dist/electron.js +19 -0
  55. package/dist/input/Actions.js +58 -27
  56. package/dist/input/InputCapture.js +98 -0
  57. package/dist/input/Keymap.js +32 -26
  58. package/dist/input/haptics.js +50 -0
  59. package/dist/input/inputTape.js +68 -0
  60. package/dist/mobile/backButton.js +40 -0
  61. package/dist/mobile/capacitor.js +48 -0
  62. package/dist/mobile/screen.js +31 -0
  63. package/dist/mobile.js +12 -0
  64. package/dist/nav/flowField.js +58 -0
  65. package/dist/nav/steering.js +77 -0
  66. package/dist/net/ApiClient.js +96 -0
  67. package/dist/net/InterpolationBuffer.js +30 -23
  68. package/dist/net/LagCompensator.js +100 -0
  69. package/dist/net/Room.js +122 -43
  70. package/dist/net/RoomHost.js +13 -4
  71. package/dist/net/SpatialHash.js +66 -0
  72. package/dist/pixel/lifeArt.js +83 -0
  73. package/dist/pixel.js +63 -58
  74. package/dist/react/ConfirmButton.js +24 -0
  75. package/dist/react/baseCss.js +1 -0
  76. package/dist/react/labels.js +7 -6
  77. package/dist/react/useJsonSelector.js +13 -0
  78. package/dist/react.js +73 -68
  79. package/dist/tilemap/TilemapView.js +57 -32
  80. package/dist/tilemap/autotile.js +103 -0
  81. package/dist/tilemap/collision.js +29 -18
  82. package/dist/tilemap/ldtk.js +21 -0
  83. package/dist/tilemap/ldtkParse.js +191 -0
  84. package/dist/types/ai/behaviorTree.d.ts +133 -0
  85. package/dist/types/ai/behaviorTree.d.ts.map +1 -0
  86. package/dist/types/ai/buildTree.d.ts +55 -0
  87. package/dist/types/ai/buildTree.d.ts.map +1 -0
  88. package/dist/types/ambient/life/Birds.d.ts +50 -0
  89. package/dist/types/ambient/life/Birds.d.ts.map +1 -0
  90. package/dist/types/ambient/life/Butterflies.d.ts +32 -0
  91. package/dist/types/ambient/life/Butterflies.d.ts.map +1 -0
  92. package/dist/types/ambient/life/ChimneySmoke.d.ts +37 -0
  93. package/dist/types/ambient/life/ChimneySmoke.d.ts.map +1 -0
  94. package/dist/types/ambient/life/Drift.d.ts +58 -0
  95. package/dist/types/ambient/life/Drift.d.ts.map +1 -0
  96. package/dist/types/ambient/life/Fireflies.d.ts +43 -0
  97. package/dist/types/ambient/life/Fireflies.d.ts.map +1 -0
  98. package/dist/types/ambient/life/lifeMotion.d.ts +63 -0
  99. package/dist/types/ambient/life/lifeMotion.d.ts.map +1 -0
  100. package/dist/types/assets/AssetLoader.d.ts +20 -0
  101. package/dist/types/assets/AssetLoader.d.ts.map +1 -1
  102. package/dist/types/assets/Prepainter.d.ts +64 -0
  103. package/dist/types/assets/Prepainter.d.ts.map +1 -0
  104. package/dist/types/assets/workerPainter.d.ts +21 -0
  105. package/dist/types/assets/workerPainter.d.ts.map +1 -0
  106. package/dist/types/audio/spatial.d.ts +107 -0
  107. package/dist/types/audio/spatial.d.ts.map +1 -0
  108. package/dist/types/dialogue/DialogueRunner.d.ts +78 -0
  109. package/dist/types/dialogue/DialogueRunner.d.ts.map +1 -0
  110. package/dist/types/dialogue/expr.d.ts +12 -0
  111. package/dist/types/dialogue/expr.d.ts.map +1 -0
  112. package/dist/types/dialogue/script.d.ts +78 -0
  113. package/dist/types/dialogue/script.d.ts.map +1 -0
  114. package/dist/types/dialogue/validateDialogue.d.ts +21 -0
  115. package/dist/types/dialogue/validateDialogue.d.ts.map +1 -0
  116. package/dist/types/ecs/prefab.d.ts +84 -0
  117. package/dist/types/ecs/prefab.d.ts.map +1 -0
  118. package/dist/types/electron/electronLike.d.ts +56 -0
  119. package/dist/types/electron/electronLike.d.ts.map +1 -0
  120. package/dist/types/electron/index.d.ts +15 -0
  121. package/dist/types/electron/index.d.ts.map +1 -0
  122. package/dist/types/electron/lockdown.d.ts +34 -0
  123. package/dist/types/electron/lockdown.d.ts.map +1 -0
  124. package/dist/types/electron/lockdownRules.d.ts +29 -0
  125. package/dist/types/electron/lockdownRules.d.ts.map +1 -0
  126. package/dist/types/electron/sessionStore.d.ts +41 -0
  127. package/dist/types/electron/sessionStore.d.ts.map +1 -0
  128. package/dist/types/electron/shellId.d.ts +23 -0
  129. package/dist/types/electron/shellId.d.ts.map +1 -0
  130. package/dist/types/electron/webFiles.d.ts +14 -0
  131. package/dist/types/electron/webFiles.d.ts.map +1 -0
  132. package/dist/types/electron/webPatcher.d.ts +84 -0
  133. package/dist/types/electron/webPatcher.d.ts.map +1 -0
  134. package/dist/types/electron/webPointers.d.ts +20 -0
  135. package/dist/types/electron/webPointers.d.ts.map +1 -0
  136. package/dist/types/index.d.ts +30 -3
  137. package/dist/types/index.d.ts.map +1 -1
  138. package/dist/types/input/Actions.d.ts +14 -0
  139. package/dist/types/input/Actions.d.ts.map +1 -1
  140. package/dist/types/input/InputCapture.d.ts +53 -0
  141. package/dist/types/input/InputCapture.d.ts.map +1 -0
  142. package/dist/types/input/Keymap.d.ts +12 -6
  143. package/dist/types/input/Keymap.d.ts.map +1 -1
  144. package/dist/types/input/haptics.d.ts +42 -0
  145. package/dist/types/input/haptics.d.ts.map +1 -0
  146. package/dist/types/input/inputTape.d.ts +66 -0
  147. package/dist/types/input/inputTape.d.ts.map +1 -0
  148. package/dist/types/mobile/backButton.d.ts +30 -0
  149. package/dist/types/mobile/backButton.d.ts.map +1 -0
  150. package/dist/types/mobile/capacitor.d.ts +62 -0
  151. package/dist/types/mobile/capacitor.d.ts.map +1 -0
  152. package/dist/types/mobile/index.d.ts +4 -0
  153. package/dist/types/mobile/index.d.ts.map +1 -0
  154. package/dist/types/mobile/screen.d.ts +27 -0
  155. package/dist/types/mobile/screen.d.ts.map +1 -0
  156. package/dist/types/nav/flowField.d.ts +31 -0
  157. package/dist/types/nav/flowField.d.ts.map +1 -0
  158. package/dist/types/nav/steering.d.ts +47 -0
  159. package/dist/types/nav/steering.d.ts.map +1 -0
  160. package/dist/types/net/ApiClient.d.ts +74 -0
  161. package/dist/types/net/ApiClient.d.ts.map +1 -0
  162. package/dist/types/net/InterpolationBuffer.d.ts +5 -0
  163. package/dist/types/net/InterpolationBuffer.d.ts.map +1 -1
  164. package/dist/types/net/LagCompensator.d.ts +70 -0
  165. package/dist/types/net/LagCompensator.d.ts.map +1 -0
  166. package/dist/types/net/Room.d.ts +72 -3
  167. package/dist/types/net/Room.d.ts.map +1 -1
  168. package/dist/types/net/RoomHost.d.ts +8 -3
  169. package/dist/types/net/RoomHost.d.ts.map +1 -1
  170. package/dist/types/net/SpatialHash.d.ts +25 -0
  171. package/dist/types/net/SpatialHash.d.ts.map +1 -0
  172. package/dist/types/pixel/index.d.ts +1 -0
  173. package/dist/types/pixel/index.d.ts.map +1 -1
  174. package/dist/types/pixel/lifeArt.d.ts +63 -0
  175. package/dist/types/pixel/lifeArt.d.ts.map +1 -0
  176. package/dist/types/react/ConfirmButton.d.ts +26 -0
  177. package/dist/types/react/ConfirmButton.d.ts.map +1 -0
  178. package/dist/types/react/baseCss.d.ts +1 -1
  179. package/dist/types/react/baseCss.d.ts.map +1 -1
  180. package/dist/types/react/index.d.ts +2 -0
  181. package/dist/types/react/index.d.ts.map +1 -1
  182. package/dist/types/react/labels.d.ts +2 -0
  183. package/dist/types/react/labels.d.ts.map +1 -1
  184. package/dist/types/react/useJsonSelector.d.ts +13 -0
  185. package/dist/types/react/useJsonSelector.d.ts.map +1 -0
  186. package/dist/types/tilemap/TilemapView.d.ts +17 -0
  187. package/dist/types/tilemap/TilemapView.d.ts.map +1 -1
  188. package/dist/types/tilemap/autotile.d.ts +95 -0
  189. package/dist/types/tilemap/autotile.d.ts.map +1 -0
  190. package/dist/types/tilemap/collision.d.ts +8 -0
  191. package/dist/types/tilemap/collision.d.ts.map +1 -1
  192. package/dist/types/tilemap/ldtk.d.ts +97 -0
  193. package/dist/types/tilemap/ldtk.d.ts.map +1 -0
  194. package/dist/types/tilemap/ldtkParse.d.ts +26 -0
  195. package/dist/types/tilemap/ldtkParse.d.ts.map +1 -0
  196. package/dist/types/ui/ScrollView.d.ts.map +1 -1
  197. package/dist/types/updates/index.d.ts +9 -0
  198. package/dist/types/updates/index.d.ts.map +1 -0
  199. package/dist/types/updates/manifest.d.ts +74 -0
  200. package/dist/types/updates/manifest.d.ts.map +1 -0
  201. package/dist/types/updates/pickUpdate.d.ts +43 -0
  202. package/dist/types/updates/pickUpdate.d.ts.map +1 -0
  203. package/dist/types/updates/webPlan.d.ts +21 -0
  204. package/dist/types/updates/webPlan.d.ts.map +1 -0
  205. package/dist/ui/ScrollView.js +10 -8
  206. package/dist/updates/manifest.js +35 -0
  207. package/dist/updates/pickUpdate.js +27 -0
  208. package/dist/updates/webPlan.js +14 -0
  209. package/dist/updates.js +15 -0
  210. package/package.json +35 -3
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebPatcher = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
7
+ const manifest_1 = require("../updates/manifest");
8
+ const webPlan_1 = require("../updates/webPlan");
9
+ const webFiles_1 = require("./webFiles");
10
+ const webPointers_1 = require("./webPointers");
11
+ /**
12
+ * A desktop shell that patches its web file by file, the way a game launcher does: the web a build ships is its
13
+ * built-in copy, and every newer web is assembled beside it in `<root>/<version>/` from the files already held plus
14
+ * only those whose content is new (`<channel>/files/<sha256>`, checked against their hash), then loaded in its place.
15
+ * A web is taken only by a shell of the same identity; any other change needs an installer. A web that never reports
16
+ * booting ({@link markBooted}, from the page) within `maxTries` starts is dropped, and the shell goes back to the last
17
+ * web that did — or the built-in one — so a broken release cannot lock a player out.
18
+ *
19
+ * Use one per process: {@link startEntry} first (it decides which web this run loads), then the update calls.
20
+ */
21
+ class WebPatcher {
22
+ options;
23
+ o;
24
+ pointers;
25
+ shellValue = null;
26
+ /** The web this run loads, and its version (the installed build's own when none is patched in). */
27
+ loaded = null;
28
+ /** The tidy a start began: a build waits for it, or it could delete the `.part` folder being filled. */
29
+ tidying = Promise.resolve();
30
+ tidyChain = Promise.resolve();
31
+ constructor(options) {
32
+ this.options = options;
33
+ this.o = {
34
+ root: options.root,
35
+ builtinDir: options.builtinDir,
36
+ appVersion: options.appVersion,
37
+ channelUrl: options.channelUrl,
38
+ packaged: options.packaged ?? true,
39
+ maxTries: options.maxTries ?? 2,
40
+ keepPrevious: options.keepPrevious ?? true,
41
+ entry: options.entry ?? 'index.html',
42
+ parallel: options.parallel ?? 6,
43
+ fetchTimeoutMs: options.fetchTimeoutMs ?? 15_000,
44
+ fetch: options.fetch ?? ((url, init) => globalThis.fetch(url, init)),
45
+ log: options.log ?? ((...a) => console.error(...a)),
46
+ };
47
+ this.pointers = new webPointers_1.WebPointers(options.root);
48
+ }
49
+ /** This shell's identity. */
50
+ get shell() {
51
+ const s = this.options.shell;
52
+ this.shellValue ??= typeof s === 'string' ? s : s();
53
+ return this.shellValue;
54
+ }
55
+ /** Where the window loads the page from: a patched web when there is a good one, else the built-in copy. */
56
+ startEntry() {
57
+ if (!this.o.packaged) {
58
+ this.loaded = { dir: this.o.builtinDir, version: this.o.appVersion };
59
+ return (0, node_path_1.join)(this.o.builtinDir, this.o.entry);
60
+ }
61
+ try {
62
+ // a web built and left for later becomes the one in use on this start
63
+ const pending = this.pointers.read('pending');
64
+ if (this.usable(pending))
65
+ this.promote(pending, 0);
66
+ this.pointers.write('pending', null);
67
+ const active = this.pointers.read('active');
68
+ if (this.usable(active) && (active.booted || active.tries < this.o.maxTries)) {
69
+ if (!active.booted)
70
+ this.pointers.write('active', { ...active, tries: active.tries + 1 });
71
+ this.loaded = { dir: (0, node_path_1.join)(this.o.root, active.version), version: active.version };
72
+ }
73
+ else {
74
+ // never booted, from another shell, or overtaken by an installer: back to the last good web, else the built-in
75
+ if (active)
76
+ this.pointers.write('active', null);
77
+ const previous = this.pointers.read('previous');
78
+ this.pointers.write('previous', null);
79
+ if (this.o.keepPrevious && previous?.booted && this.usable(previous)) {
80
+ this.pointers.write('active', previous);
81
+ this.loaded = { dir: (0, node_path_1.join)(this.o.root, previous.version), version: previous.version };
82
+ }
83
+ }
84
+ }
85
+ catch (err) {
86
+ this.o.log('[web] could not read the patched web, using the built-in one', err);
87
+ }
88
+ this.loaded ??= { dir: this.o.builtinDir, version: this.o.appVersion };
89
+ this.tidying = this.tidy();
90
+ return (0, node_path_1.join)(this.loaded.dir, this.o.entry);
91
+ }
92
+ /** The version of the web running now. */
93
+ webVersion() {
94
+ return this.loaded?.version ?? this.o.appVersion;
95
+ }
96
+ /** The page came up: the web it was loaded from is kept from now on. Call it from the page's own "booted" message. */
97
+ markBooted() {
98
+ const active = this.pointers.read('active');
99
+ if (active && this.loaded && active.version === this.loaded.version && !active.booted)
100
+ this.pointers.write('active', { ...active, booted: true });
101
+ }
102
+ /** The channel's newest build, or null when the store cannot be read. */
103
+ async latest() {
104
+ try {
105
+ const res = await (0, webFiles_1.fetchWithTimeout)(this.o.fetch, `${this.o.channelUrl}/latest.json`, this.o.fetchTimeoutMs);
106
+ if (!res.ok)
107
+ return null;
108
+ const l = (await res.json());
109
+ return typeof l?.version === 'string' ? l : null;
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
115
+ /** Whether `l`'s web can be patched in here rather than installed: newer than the web running, for this shell. */
116
+ patchable(l) {
117
+ return !!l.web && l.web.shell === this.shell && (0, manifest_1.isNewer)(l.version, this.webVersion());
118
+ }
119
+ /**
120
+ * Builds `l`'s web beside the one running: the files held are copied, the rest downloaded and checked, into
121
+ * `<version>.part/`, which becomes `<version>/` and the pending web once whole — a crash mid-way never leaves a
122
+ * folder that looks finished. `onProgress` gets 0–100. Resolves with the version built; throws on any failure.
123
+ */
124
+ async prepare(l, onProgress = () => undefined) {
125
+ if (!l.web)
126
+ throw new Error('no web in this release');
127
+ const res = await (0, webFiles_1.fetchWithTimeout)(this.o.fetch, `${this.o.channelUrl}/${l.web.manifest}`, this.o.fetchTimeoutMs);
128
+ if (!res.ok)
129
+ throw new Error(`web manifest ${res.status}`);
130
+ const target = await res.json();
131
+ if (!(0, manifest_1.validManifest)(target) || target.version !== l.version || target.shell !== this.shell)
132
+ throw new Error('bad web manifest');
133
+ const done = (0, node_path_1.join)(this.o.root, target.version);
134
+ const pointer = { version: target.version, shell: target.shell, booted: false, tries: 0 };
135
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(done, this.o.entry)) && this.pointers.read('active')?.version !== target.version) {
136
+ // already assembled by an earlier run that stopped before it was put in use
137
+ this.pointers.write('pending', pointer);
138
+ onProgress(100);
139
+ return target.version;
140
+ }
141
+ await this.tidying;
142
+ const from = this.loaded?.dir ?? this.o.builtinDir;
143
+ const plan = (0, webPlan_1.planWeb)(target, await (0, webFiles_1.hashWeb)(from));
144
+ const part = (0, node_path_1.join)(this.o.root, `${target.version}.part`);
145
+ await (0, promises_1.rm)(part, { recursive: true, force: true });
146
+ await (0, promises_1.mkdir)(part, { recursive: true });
147
+ for (const c of plan.copy)
148
+ await (0, webFiles_1.putFile)(part, c.path, await (0, promises_1.readFile)((0, node_path_1.join)(from, ...c.from.split('/'))));
149
+ let got = 0;
150
+ const fetched = new Map();
151
+ const queue = [...plan.download];
152
+ onProgress(plan.bytes ? 0 : 100);
153
+ const worker = async () => {
154
+ for (let f = queue.shift(); f; f = queue.shift()) {
155
+ let data = fetched.get(f.sha256);
156
+ if (!data) {
157
+ const r = await (0, webFiles_1.fetchWithTimeout)(this.o.fetch, (0, manifest_1.fileUrl)(this.o.channelUrl, f.sha256), this.o.fetchTimeoutMs);
158
+ if (!r.ok)
159
+ throw new Error(`${f.path}: ${r.status}`);
160
+ data = new Uint8Array(await r.arrayBuffer());
161
+ if ((0, webFiles_1.sha256)(data) !== f.sha256)
162
+ throw new Error(`${f.path}: checksum`);
163
+ fetched.set(f.sha256, data);
164
+ got += f.size;
165
+ onProgress(Math.min(99, Math.round((got / Math.max(1, plan.bytes)) * 100)));
166
+ }
167
+ await (0, webFiles_1.putFile)(part, f.path, data);
168
+ }
169
+ };
170
+ await Promise.all(Array.from({ length: Math.max(1, this.o.parallel) }, worker));
171
+ await (0, promises_1.rm)(done, { recursive: true, force: true });
172
+ await (0, promises_1.rename)(part, done);
173
+ this.pointers.write('pending', pointer);
174
+ onProgress(100);
175
+ return target.version;
176
+ }
177
+ /**
178
+ * Puts the pending web in use now and returns the page to load (reload every window from it), or null when there is
179
+ * none this shell may load. The switch counts as its first start, so a web that never boots is still dropped.
180
+ */
181
+ applyPending() {
182
+ const pending = this.pointers.read('pending');
183
+ if (!this.usable(pending))
184
+ return null;
185
+ this.promote(pending, 1);
186
+ this.pointers.write('pending', null);
187
+ this.loaded = { dir: (0, node_path_1.join)(this.o.root, pending.version), version: pending.version };
188
+ return (0, node_path_1.join)(this.loaded.dir, this.o.entry);
189
+ }
190
+ /** Makes `p` the active web, keeping the one it replaces as the fallback when that one ever booted. */
191
+ promote(p, tries) {
192
+ const active = this.pointers.read('active');
193
+ if (this.o.keepPrevious && active?.booted && active.version !== p.version)
194
+ this.pointers.write('previous', active);
195
+ this.pointers.write('active', { ...p, booted: false, tries });
196
+ }
197
+ /** A pointer this shell may load: its own shell, newer than the installed build, its folder whole. */
198
+ usable(p) {
199
+ return !!p && p.shell === this.shell && (0, manifest_1.isNewer)(p.version, this.o.appVersion) && (0, node_fs_1.existsSync)((0, node_path_1.join)(this.o.root, p.version, this.o.entry));
200
+ }
201
+ /** Removes every patched web but the active, pending and previous one, and any half-made folder. A call made while
202
+ * one is running waits for it and then runs once more, so two never delete the same folder at once. */
203
+ tidy() {
204
+ const next = this.tidyChain.then(() => this.tidyOnce());
205
+ this.tidyChain = next;
206
+ return next;
207
+ }
208
+ async tidyOnce() {
209
+ try {
210
+ const names = ['active', 'pending', 'previous'];
211
+ const keep = new Set([...names.map((n) => this.pointers.read(n)?.version), this.loaded?.version].filter((v) => !!v));
212
+ for (const name of await (0, promises_1.readdir)(this.o.root)) {
213
+ if (name.endsWith('.json') || keep.has(name))
214
+ continue;
215
+ await (0, promises_1.rm)((0, node_path_1.join)(this.o.root, name), { recursive: true, force: true });
216
+ }
217
+ }
218
+ catch {
219
+ // no web folder yet
220
+ }
221
+ }
222
+ }
223
+ exports.WebPatcher = WebPatcher;
@@ -0,0 +1,19 @@
1
+ /** Which patched web a pointer names, and whether it ever came up. */
2
+ export interface WebPointer {
3
+ version: string;
4
+ shell: string;
5
+ /** The page reported booting from it at least once. */
6
+ booted: boolean;
7
+ /** Starts it was loaded on without booting. */
8
+ tries: number;
9
+ }
10
+ /** `active`: in use; `pending`: built, put in use on the next start; `previous`: the last web that booted before `active`. */
11
+ export type PointerName = 'active' | 'pending' | 'previous';
12
+ /** The pointer files beside the patched webs (`<root>/<name>.json`), read and written whole. */
13
+ export declare class WebPointers {
14
+ private readonly root;
15
+ constructor(root: string);
16
+ read(name: PointerName): WebPointer | null;
17
+ write(name: PointerName, p: WebPointer | null): void;
18
+ private file;
19
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebPointers = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ /** The pointer files beside the patched webs (`<root>/<name>.json`), read and written whole. */
7
+ class WebPointers {
8
+ root;
9
+ constructor(root) {
10
+ this.root = root;
11
+ }
12
+ read(name) {
13
+ try {
14
+ const p = JSON.parse((0, node_fs_1.readFileSync)(this.file(name), 'utf8'));
15
+ if (typeof p?.version !== 'string' || typeof p.shell !== 'string')
16
+ return null;
17
+ return { version: p.version, shell: p.shell, booted: p.booted === true, tries: typeof p.tries === 'number' ? p.tries : 0 };
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ write(name, p) {
24
+ if (p) {
25
+ (0, node_fs_1.mkdirSync)(this.root, { recursive: true });
26
+ (0, node_fs_1.writeFileSync)(this.file(name), JSON.stringify(p));
27
+ }
28
+ else
29
+ (0, node_fs_1.rmSync)(this.file(name), { force: true });
30
+ }
31
+ file(name) {
32
+ return (0, node_path_1.join)(this.root, `${name}.json`);
33
+ }
34
+ }
35
+ exports.WebPointers = WebPointers;
@@ -0,0 +1 @@
1
+ { "type": "commonjs" }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `1681-engine/updates`: file-by-file web updates from a release store, the pure half — the feed's shapes, which web
3
+ * to take, and what to copy or download to build it. No Node, DOM or Electron: shared by the desktop shell
4
+ * (`1681-engine/electron`), a browser build and the phone apps.
5
+ */
6
+ export { isNewer, safePath, isSha256, validManifest, fileUrl, type WebFile, type WebManifest, type LatestJson, type ReleaseFile, } from './manifest';
7
+ export { planWeb, type WebPlan } from './webPlan';
8
+ export { pickWebUpdate, apkUpdateOf, pluginEntries, heldBundle, type WebBundle, type NewerRelease, type PluginEntry } from './pickUpdate';
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.heldBundle = exports.pluginEntries = exports.apkUpdateOf = exports.pickWebUpdate = exports.planWeb = exports.fileUrl = exports.validManifest = exports.isSha256 = exports.safePath = exports.isNewer = void 0;
4
+ /**
5
+ * `1681-engine/updates`: file-by-file web updates from a release store, the pure half — the feed's shapes, which web
6
+ * to take, and what to copy or download to build it. No Node, DOM or Electron: shared by the desktop shell
7
+ * (`1681-engine/electron`), a browser build and the phone apps.
8
+ */
9
+ var manifest_1 = require("./manifest");
10
+ Object.defineProperty(exports, "isNewer", { enumerable: true, get: function () { return manifest_1.isNewer; } });
11
+ Object.defineProperty(exports, "safePath", { enumerable: true, get: function () { return manifest_1.safePath; } });
12
+ Object.defineProperty(exports, "isSha256", { enumerable: true, get: function () { return manifest_1.isSha256; } });
13
+ Object.defineProperty(exports, "validManifest", { enumerable: true, get: function () { return manifest_1.validManifest; } });
14
+ Object.defineProperty(exports, "fileUrl", { enumerable: true, get: function () { return manifest_1.fileUrl; } });
15
+ var webPlan_1 = require("./webPlan");
16
+ Object.defineProperty(exports, "planWeb", { enumerable: true, get: function () { return webPlan_1.planWeb; } });
17
+ var pickUpdate_1 = require("./pickUpdate");
18
+ Object.defineProperty(exports, "pickWebUpdate", { enumerable: true, get: function () { return pickUpdate_1.pickWebUpdate; } });
19
+ Object.defineProperty(exports, "apkUpdateOf", { enumerable: true, get: function () { return pickUpdate_1.apkUpdateOf; } });
20
+ Object.defineProperty(exports, "pluginEntries", { enumerable: true, get: function () { return pickUpdate_1.pluginEntries; } });
21
+ Object.defineProperty(exports, "heldBundle", { enumerable: true, get: function () { return pickUpdate_1.heldBundle; } });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The shapes of a release store's feed for file-by-file web updates, and the rules for trusting one. One definition
3
+ * for every client that reads it: the desktop shell's patcher (`1681-engine/electron`), a browser build and the phone
4
+ * apps' over-the-air updater. A publish script writes, per channel:
5
+ *
6
+ * - `<channel>/latest.json` ({@link LatestJson}): the newest build and where its pieces lie, as paths under the channel,
7
+ * written last so a client never reads a release that is half up;
8
+ * - `<channel>/web/<version>.json` ({@link WebManifest}): every file of the built web with its sha256;
9
+ * - `<channel>/files/<sha256>`: each content once, so a release uploads and a client downloads only what is new.
10
+ *
11
+ * Kept pure (no Node, no DOM, no Electron) so a phone's web view and an Electron main process share it.
12
+ */
13
+ /** One file of a built web, as the manifest lists it. */
14
+ export interface WebFile {
15
+ /** Relative to the web's root, forward slashes (`assets/index-3f2a.js`). */
16
+ path: string;
17
+ /** Lower-case hex sha256 of the content; also its name under `<channel>/files/`. */
18
+ sha256: string;
19
+ size: number;
20
+ }
21
+ /** `<channel>/web/<version>.json`. */
22
+ export interface WebManifest {
23
+ version: string;
24
+ /**
25
+ * The desktop shell this web needs (`shellId` in `1681-engine/electron`): a web talks to its shell through a
26
+ * preload script and may need what a newer shell adds, so any other shell takes the installer instead.
27
+ */
28
+ shell: string;
29
+ /** The lowest native build of the phone apps this web needs: a web calling a plugin the app lacks must never reach it. */
30
+ minNative: number;
31
+ files: WebFile[];
32
+ }
33
+ /** A packaged download listed in `latest.json` (an APK, an installer). */
34
+ export interface ReleaseFile {
35
+ file: string;
36
+ size: number;
37
+ sha256?: string;
38
+ }
39
+ /** `<channel>/latest.json`. */
40
+ export interface LatestJson {
41
+ version: string;
42
+ date?: string;
43
+ /** The build's web: its manifest's path under the channel, the shell and native build it needs. */
44
+ web?: {
45
+ manifest: string;
46
+ shell: string;
47
+ minNative: number;
48
+ };
49
+ /** The Android package, when the channel publishes one. */
50
+ apk?: {
51
+ file: string;
52
+ size: number;
53
+ sha256: string;
54
+ };
55
+ /** Desktop installers by platform key (the publisher names the keys, e.g. `win`, `macArm`, `macX64`). */
56
+ desktop?: Record<string, ReleaseFile | undefined>;
57
+ }
58
+ /** Whether version `a` ("0.5.23", "v0.5.23") is newer than `b`, number by number; a missing part counts 0. */
59
+ export declare function isNewer(a: string, b: string): boolean;
60
+ /**
61
+ * A path a manifest may name: relative, forward slashes, no `.` or `..` segment, no NUL — nothing that could write
62
+ * outside the web's own folder once joined to it.
63
+ */
64
+ export declare function safePath(p: string): boolean;
65
+ /** Whether `s` is a lower-case hex sha256. */
66
+ export declare function isSha256(s: unknown): s is string;
67
+ /**
68
+ * A manifest whose shape may be trusted: every path safe and listed once, every hash a sha256, every size a number,
69
+ * and an `index.html` (a web without one cannot be loaded, and a truncated upload is the usual way to get one).
70
+ */
71
+ export declare function validManifest(m: unknown): m is WebManifest;
72
+ /** Where a content lies in the store: `<channelUrl>/files/<sha256>`. */
73
+ export declare function fileUrl(channelUrl: string, sha256: string): string;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * The shapes of a release store's feed for file-by-file web updates, and the rules for trusting one. One definition
4
+ * for every client that reads it: the desktop shell's patcher (`1681-engine/electron`), a browser build and the phone
5
+ * apps' over-the-air updater. A publish script writes, per channel:
6
+ *
7
+ * - `<channel>/latest.json` ({@link LatestJson}): the newest build and where its pieces lie, as paths under the channel,
8
+ * written last so a client never reads a release that is half up;
9
+ * - `<channel>/web/<version>.json` ({@link WebManifest}): every file of the built web with its sha256;
10
+ * - `<channel>/files/<sha256>`: each content once, so a release uploads and a client downloads only what is new.
11
+ *
12
+ * Kept pure (no Node, no DOM, no Electron) so a phone's web view and an Electron main process share it.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.isNewer = isNewer;
16
+ exports.safePath = safePath;
17
+ exports.isSha256 = isSha256;
18
+ exports.validManifest = validManifest;
19
+ exports.fileUrl = fileUrl;
20
+ /** Whether version `a` ("0.5.23", "v0.5.23") is newer than `b`, number by number; a missing part counts 0. */
21
+ function isNewer(a, b) {
22
+ const parse = (v) => v.replace(/^v/, '').split('.').map((n) => Number.parseInt(n, 10) || 0);
23
+ const x = parse(a);
24
+ const y = parse(b);
25
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
26
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
27
+ if (d !== 0)
28
+ return d > 0;
29
+ }
30
+ return false;
31
+ }
32
+ /**
33
+ * A path a manifest may name: relative, forward slashes, no `.` or `..` segment, no NUL — nothing that could write
34
+ * outside the web's own folder once joined to it.
35
+ */
36
+ function safePath(p) {
37
+ if (!p || p.startsWith('/') || p.includes('\\') || p.includes('\0'))
38
+ return false;
39
+ return p.split('/').every((seg) => seg !== '' && seg !== '.' && seg !== '..');
40
+ }
41
+ const SHA256 = /^[0-9a-f]{64}$/;
42
+ /** Whether `s` is a lower-case hex sha256. */
43
+ function isSha256(s) {
44
+ return typeof s === 'string' && SHA256.test(s);
45
+ }
46
+ /**
47
+ * A manifest whose shape may be trusted: every path safe and listed once, every hash a sha256, every size a number,
48
+ * and an `index.html` (a web without one cannot be loaded, and a truncated upload is the usual way to get one).
49
+ */
50
+ function validManifest(m) {
51
+ const x = m;
52
+ if (!x || typeof x.version !== 'string' || typeof x.shell !== 'string' || !Array.isArray(x.files))
53
+ return false;
54
+ const seen = new Set();
55
+ for (const f of x.files) {
56
+ if (!f || typeof f.path !== 'string' || !safePath(f.path) || seen.has(f.path))
57
+ return false;
58
+ if (!isSha256(f.sha256) || typeof f.size !== 'number')
59
+ return false;
60
+ seen.add(f.path);
61
+ }
62
+ return seen.has('index.html');
63
+ }
64
+ /** Where a content lies in the store: `<channelUrl>/files/<sha256>`. */
65
+ function fileUrl(channelUrl, sha256) {
66
+ return `${channelUrl}/files/${sha256}`;
67
+ }
@@ -0,0 +1,42 @@
1
+ import { type LatestJson } from './manifest';
2
+ /** A web a client may take: its version, its manifest's full URL and the native build it needs. */
3
+ export interface WebBundle {
4
+ version: string;
5
+ manifestUrl: string;
6
+ minNative: number;
7
+ }
8
+ /**
9
+ * The web to take from `latest`: newer than the web running (`webVersion`), and needing no native code this app lacks
10
+ * (`nativeBuild` ≥ the web's `minNative`). A browser or desktop build with no native layer passes `Infinity`.
11
+ */
12
+ export declare function pickWebUpdate(latest: LatestJson, channelUrl: string, webVersion: string, nativeBuild: number): WebBundle | null;
13
+ /** A newer build than this one, and where its Android package downloads from. */
14
+ export interface NewerRelease {
15
+ version: string;
16
+ apkUrl: string;
17
+ }
18
+ /**
19
+ * The build's APK, when it is newer than this client and its web cannot come over the air instead (`webBundle` is
20
+ * what {@link pickWebUpdate} found): the package is the slow road, for native changes.
21
+ */
22
+ export declare function apkUpdateOf(latest: LatestJson, channelUrl: string, clientVersion: string, webBundle: WebBundle | null): NewerRelease | null;
23
+ /** One entry of a multi-file bundle download, in the shape `@capgo/capacitor-updater`'s `ManifestEntry` takes. */
24
+ export interface PluginEntry {
25
+ file_name: string;
26
+ file_hash: string;
27
+ download_url: string;
28
+ }
29
+ /**
30
+ * A phone updater's download list for `manifest`, or null when the manifest is not one to trust (another version, an
31
+ * unsafe or repeated path, a bad hash, no `index.html`). The updater checks each file against its hash and reuses
32
+ * every file it already holds, so only new content is fetched.
33
+ */
34
+ export declare function pluginEntries(manifest: unknown, version: string, channelUrl: string): PluginEntry[] | null;
35
+ /**
36
+ * A copy of `bundle` the updater already holds whole (downloaded and checked, applied or waiting), so asking again —
37
+ * a settings screen's check after the login screen's — takes it instead of downloading the same files twice.
38
+ */
39
+ export declare function heldBundle<T extends {
40
+ version: string;
41
+ status: string;
42
+ }>(held: readonly T[], bundle: WebBundle): T | null;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pickWebUpdate = pickWebUpdate;
4
+ exports.apkUpdateOf = apkUpdateOf;
5
+ exports.pluginEntries = pluginEntries;
6
+ exports.heldBundle = heldBundle;
7
+ const manifest_1 = require("./manifest");
8
+ /**
9
+ * The web to take from `latest`: newer than the web running (`webVersion`), and needing no native code this app lacks
10
+ * (`nativeBuild` ≥ the web's `minNative`). A browser or desktop build with no native layer passes `Infinity`.
11
+ */
12
+ function pickWebUpdate(latest, channelUrl, webVersion, nativeBuild) {
13
+ const w = latest.web;
14
+ if (!w || !(0, manifest_1.isNewer)(latest.version, webVersion) || nativeBuild < w.minNative)
15
+ return null;
16
+ return { version: latest.version, manifestUrl: `${channelUrl}/${w.manifest}`, minNative: w.minNative };
17
+ }
18
+ /**
19
+ * The build's APK, when it is newer than this client and its web cannot come over the air instead (`webBundle` is
20
+ * what {@link pickWebUpdate} found): the package is the slow road, for native changes.
21
+ */
22
+ function apkUpdateOf(latest, channelUrl, clientVersion, webBundle) {
23
+ if (webBundle || !latest.apk || !(0, manifest_1.isNewer)(latest.version, clientVersion))
24
+ return null;
25
+ return { version: latest.version, apkUrl: `${channelUrl}/${latest.apk.file}` };
26
+ }
27
+ /**
28
+ * A phone updater's download list for `manifest`, or null when the manifest is not one to trust (another version, an
29
+ * unsafe or repeated path, a bad hash, no `index.html`). The updater checks each file against its hash and reuses
30
+ * every file it already holds, so only new content is fetched.
31
+ */
32
+ function pluginEntries(manifest, version, channelUrl) {
33
+ const m = manifest;
34
+ if (!m || m.version !== version || !Array.isArray(m.files))
35
+ return null;
36
+ const out = [];
37
+ const seen = new Set();
38
+ for (const f of m.files) {
39
+ if (!f || typeof f.path !== 'string' || !(0, manifest_1.safePath)(f.path) || seen.has(f.path) || !(0, manifest_1.isSha256)(f.sha256))
40
+ return null;
41
+ seen.add(f.path);
42
+ out.push({ file_name: f.path, file_hash: f.sha256, download_url: (0, manifest_1.fileUrl)(channelUrl, f.sha256) });
43
+ }
44
+ return seen.has('index.html') ? out : null;
45
+ }
46
+ /**
47
+ * A copy of `bundle` the updater already holds whole (downloaded and checked, applied or waiting), so asking again —
48
+ * a settings screen's check after the login screen's — takes it instead of downloading the same files twice.
49
+ */
50
+ function heldBundle(held, bundle) {
51
+ return held.find((b) => b.version === bundle.version && (b.status === 'pending' || b.status === 'success')) ?? null;
52
+ }
@@ -0,0 +1,20 @@
1
+ import type { WebFile, WebManifest } from './manifest';
2
+ /** What building a new web takes, given the files a client already holds. */
3
+ export interface WebPlan {
4
+ /** Files taken from the web held now: `from` is their path there. */
5
+ copy: {
6
+ path: string;
7
+ from: string;
8
+ }[];
9
+ /** Files to fetch from `<channel>/files/<sha256>`; content shared by several paths appears once per path. */
10
+ download: WebFile[];
11
+ /** Bytes to download, each distinct content counted once. */
12
+ bytes: number;
13
+ }
14
+ /**
15
+ * What to copy and what to download for `target`, given the web held now (`have`: path → sha256): a file whose path
16
+ * holds the same content is copied from there, one whose content lies under another path (a bundler renamed it) is
17
+ * copied from that path, and only content found nowhere is downloaded — once, however many paths share it. This is
18
+ * what makes a patch cost only what changed, the way a game launcher patches.
19
+ */
20
+ export declare function planWeb(target: WebManifest, have: ReadonlyMap<string, string>): WebPlan;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planWeb = planWeb;
4
+ /**
5
+ * What to copy and what to download for `target`, given the web held now (`have`: path → sha256): a file whose path
6
+ * holds the same content is copied from there, one whose content lies under another path (a bundler renamed it) is
7
+ * copied from that path, and only content found nowhere is downloaded — once, however many paths share it. This is
8
+ * what makes a patch cost only what changed, the way a game launcher patches.
9
+ */
10
+ function planWeb(target, have) {
11
+ const byHash = new Map();
12
+ for (const [path, sha] of have)
13
+ if (!byHash.has(sha))
14
+ byHash.set(sha, path);
15
+ const copy = [];
16
+ const download = [];
17
+ const fetched = new Set();
18
+ let bytes = 0;
19
+ for (const f of target.files) {
20
+ const there = byHash.get(f.sha256);
21
+ if (have.get(f.path) === f.sha256)
22
+ copy.push({ path: f.path, from: f.path });
23
+ else if (there !== undefined)
24
+ copy.push({ path: f.path, from: there });
25
+ else {
26
+ download.push(f);
27
+ if (!fetched.has(f.sha256)) {
28
+ fetched.add(f.sha256);
29
+ bytes += f.size;
30
+ }
31
+ }
32
+ }
33
+ return { copy, download, bytes };
34
+ }