@sasjs/cli 6.30.0 → 6.30.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.
@@ -12,6 +12,12 @@
12
12
  * same tags, the same order, and the same globals, just later than the parser
13
13
  * would have run them.
14
14
  *
15
+ * Two of those start before the worker is ready, because neither needs it: the
16
+ * drive restore is the page's own read of IndexedDB and is handed over as soon
17
+ * as there is a worker, and the app's bundle is hinted by the page (a preload
18
+ * link the builder writes). Both then overlap the install instead of following
19
+ * it.
20
+ *
15
21
  * The tag list arrives in the `data-scripts` attribute as JSON, written by
16
22
  * buildMockSite. `defer` and `async` are not replayed: execution is sequenced
17
23
  * on purpose here, because the host has to be ready before the first request
@@ -81,10 +87,26 @@
81
87
  })
82
88
  }
83
89
 
90
+ /*
91
+ * The drive restore is the page's own work: persist.js reads the stored drive
92
+ * and hands it to the worker as soon as there is one, so it neither needs nor
93
+ * waits for the registration below. Started here, its download and its
94
+ * IndexedDB read overlap the worker's install, which is the slowest part of a
95
+ * first visit - sequenced after it, they are two more round trips on top.
96
+ */
97
+ var persist = new Promise(function (resolve) {
98
+ load('host/persist.js', resolve)
99
+ })
100
+
84
101
  function start() {
85
- load('host/persist.js', function () {
102
+ persist.then(function () {
86
103
  Promise.resolve(window.__sasjsMockReady).then(function (boot) {
87
104
  if (boot && boot.error) console.error('[mock] boot: ' + boot.error)
105
+ // the drive was restored from less than the whole capture: the page still runs,
106
+ // but every file it did not already hold will read as missing, and nothing else
107
+ // says so
108
+ if (boot && boot.driveError)
109
+ console.error('[mock] drive: ' + boot.driveError)
88
110
  load('host/intercept.js', function () {
89
111
  replayAll(specs, function () {})
90
112
  })
@@ -18,6 +18,13 @@
18
18
  */
19
19
  const IDENTITY = 'identity'
20
20
  const IDENTITY_KEY = 'current'
21
+ /**
22
+ * The drive paths the visitor deleted, kept beside the identity rather than in
23
+ * the files store because a deleted file has no row to keep it in. A sharded
24
+ * site's index lists every file the build ships, so this list is what stops a
25
+ * deletion from being undone by the next page load.
26
+ */
27
+ const DELETED_KEY = 'deleted'
21
28
 
22
29
  const openDb = () =>
23
30
  new Promise((resolve, reject) => {
@@ -81,24 +88,70 @@
81
88
  }
82
89
  })
83
90
 
91
+ /** The drive paths this visitor deleted, `[]` when the store is new or unreadable. */
92
+ const readDeleted = async () => {
93
+ try {
94
+ const db = await openDb()
95
+ return await new Promise((resolve) => {
96
+ const req = db
97
+ .transaction(IDENTITY, 'readonly')
98
+ .objectStore(IDENTITY)
99
+ .get(DELETED_KEY)
100
+ req.onsuccess = () =>
101
+ resolve(Array.isArray(req.result) ? req.result : [])
102
+ req.onerror = () => resolve([])
103
+ })
104
+ } catch (e) {
105
+ return []
106
+ }
107
+ }
108
+
109
+ // no tombstones is no row: an empty list left behind would read as a stored
110
+ // drive that had something in it
111
+ const writeDeleted = (paths) =>
112
+ paths.length
113
+ ? withStore(
114
+ 'readwrite',
115
+ (store) => store.put(paths, DELETED_KEY),
116
+ IDENTITY
117
+ )
118
+ : withStore('readwrite', (store) => store.delete(DELETED_KEY), IDENTITY)
119
+
84
120
  const writeFiles = async (files) => {
85
121
  const entries = Object.entries(files || {})
86
122
  if (!entries.length) return { written: 0, deleted: 0 }
87
123
  let deleted = 0
124
+ const removed = []
125
+ const written = []
88
126
  await withStore('readwrite', (store) => {
89
127
  for (const [path, contents] of entries) {
90
128
  if (contents === null || contents === undefined) {
91
129
  store.delete(path)
130
+ removed.push(path)
92
131
  deleted++
93
132
  } else {
94
133
  store.put(contents, path)
134
+ written.push(path)
95
135
  }
96
136
  }
97
137
  })
138
+ // the worker forgets deletions when it is terminated, so they are recorded
139
+ // here: a path written again is not deleted any more, and a path deleted
140
+ // again stays recorded
141
+ if (removed.length || written.length) {
142
+ const known = (await readDeleted()).filter(
143
+ (path) => !written.includes(path)
144
+ )
145
+ for (const path of removed) if (!known.includes(path)) known.push(path)
146
+ await writeDeleted(known)
147
+ }
98
148
  return { written: entries.length - deleted, deleted }
99
149
  }
100
150
 
101
- const clearAll = () => withStore('readwrite', (store) => store.clear())
151
+ const clearAll = async () => {
152
+ await withStore('readwrite', (store) => store.clear())
153
+ await withStore('readwrite', (store) => store.delete(DELETED_KEY), IDENTITY)
154
+ }
102
155
 
103
156
  /**
104
157
  * The stored identity, `null` when there is none. A row whose shape is not the
@@ -334,6 +387,12 @@
334
387
  { type: 'sasjs-snapshot' },
335
388
  'sasjs-snapshot'
336
389
  )
390
+ // the worker answers `sasjs-snapshot` with whatever is in memory plus an
391
+ // `error` when it could not read the drive whole. Downloading that would
392
+ // hand the next build a capture with every app bundle missing and no sign
393
+ // of it, which is the failure "whole means whole" exists to prevent.
394
+ if (snapshot && snapshot.error)
395
+ throw new Error('The drive could not be read whole: ' + snapshot.error)
337
396
  const files = (snapshot && snapshot.files) || {}
338
397
  // the format stamp the runtime checks on load: a snapshot that outlived a
339
398
  // marker-format change is refused by name, instead of decoding as text
@@ -369,15 +428,23 @@
369
428
  * row holds the accounts the visitor created as well as the fixture, so a
370
429
  * host-side failure (a drive that could not be read) leaves it alone.
371
430
  */
372
- const boot = async (files, stored) => {
373
- const loading = Object.keys(files).length > 0
431
+ const boot = async (files, stored, deleted) => {
432
+ // a drive of nothing but deletions is still a drive the visitor had: seeding
433
+ // over it would bring back every file they removed
434
+ const loading =
435
+ Object.keys(files).length > 0 || (deleted && deleted.length > 0)
374
436
  const type = loading ? 'sasjs-load' : 'sasjs-seed'
375
437
  const ack = loading ? 'sasjs-load-ack' : 'sasjs-seed-ack'
376
438
 
439
+ // only when there are some: an empty list is the common case, and the worker
440
+ // reads an absent one as "nothing was deleted"
441
+ const deletions = loading && deleted.length ? deleted : undefined
442
+
377
443
  const answer = await askWorker(
378
444
  {
379
445
  type,
380
446
  files,
447
+ deleted: deletions,
381
448
  identity: stored ? stored.fixture : undefined,
382
449
  subject: stored ? stored.subject : undefined
383
450
  },
@@ -391,7 +458,7 @@
391
458
  } catch (e) {
392
459
  console.error('[sasjs-mock] could not clear the stored identity: ' + e)
393
460
  }
394
- return await askWorker({ type, files }, ack)
461
+ return await askWorker({ type, files, deleted: deletions }, ack)
395
462
  }
396
463
 
397
464
  return answer
@@ -442,7 +509,11 @@
442
509
  stored = null
443
510
  }
444
511
 
445
- window.__sasjsMockBoot = await boot(files, stored)
512
+ // the deletions go with the drive: a sharded site's index lists every file
513
+ // the build ships, so the paths this visitor removed have to be named or the
514
+ // page would restore them
515
+ const deleted = await readDeleted()
516
+ window.__sasjsMockBoot = await boot(files, stored, deleted)
446
517
  if (window.__sasjsMockBoot && !window.__sasjsMockBoot.error)
447
518
  keepBuildStamp(stamp)
448
519
  return window.__sasjsMockBoot