@sasjs/cli 6.29.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.
@@ -147,7 +147,11 @@ var loadWorker = function (base, options) {
147
147
  }
148
148
  };
149
149
  global.self = self;
150
+ // what the worker pulled into itself, so a test can assert that a script it
151
+ // does not need at boot is not fetched at boot
152
+ var imported = [];
150
153
  global.importScripts = function (path) {
154
+ imported.push(path);
151
155
  if (path.includes('md5'))
152
156
  return;
153
157
  if (path.includes('api-browser'))
@@ -185,7 +189,7 @@ var loadWorker = function (base, options) {
185
189
  };
186
190
  require('../assets/sw.js');
187
191
  global.fetch = jest.fn(function (input) { return __awaiter(void 0, void 0, void 0, function () {
188
- var target, fixture, fixture;
192
+ var target, fixture, fixture, shards, piece, id, requested, shards_1;
189
193
  var _a;
190
194
  return __generator(this, function (_b) {
191
195
  target = typeof input === 'string' ? input : (input && input.url) || String(input);
@@ -222,7 +226,29 @@ var loadWorker = function (base, options) {
222
226
  userId: mockSite_1.WEBOUT_SENTINELS.userId,
223
227
  apiServerUrl: mockSite_1.WEBOUT_SENTINELS.apiServerUrl
224
228
  })
225
- })];
229
+ })
230
+ // a sharded drive, when a test sets one up: the index answers the snapshot
231
+ // URL and each piece answers its own file, so the worker's fetch of a shard
232
+ // is the same request it makes against a static host
233
+ ];
234
+ shards = global.__sasjsShards;
235
+ if (shards && target.includes('vfs-snapshot')) {
236
+ piece = /vfs-snapshot-(\d+)\.json/.exec(target);
237
+ if (piece) {
238
+ id = Number(piece[1]);
239
+ requested = (global.__sasjsShardFetches =
240
+ global.__sasjsShardFetches || []);
241
+ requested.push(id);
242
+ return [2 /*return*/, jsonResponse({ format: mockSite_1.DRIVE_FORMAT, files: shards[id] || {} })];
243
+ }
244
+ return [2 /*return*/, jsonResponse(shards.index)];
245
+ }
246
+ if (target.includes('mock-config')) {
247
+ shards_1 = global.__sasjsShards;
248
+ return [2 /*return*/, jsonResponse(__assign({ appLoc: appLoc }, (shards_1
249
+ ? { snapshotShards: (shards_1.index.shards || []).length }
250
+ : {})))];
251
+ }
226
252
  return [2 /*return*/, target.includes('vfs-seed')
227
253
  ? jsonResponse({
228
254
  files: (_a = {},
@@ -266,6 +292,7 @@ var loadWorker = function (base, options) {
266
292
  dispatch: dispatch,
267
293
  message: message,
268
294
  lifecycle: lifecycle,
295
+ imported: imported,
269
296
  self: self,
270
297
  refuseWrites: function (value) {
271
298
  refusingWrites = value;
@@ -308,6 +335,8 @@ describe('service worker host', function () {
308
335
  delete global.importScripts;
309
336
  delete global.fetch;
310
337
  delete global.__sasjsIdentity;
338
+ delete global.__sasjsShards;
339
+ delete global.__sasjsShardFetches;
311
340
  delete global.__mockHostClients;
312
341
  });
313
342
  var seeded = function () {
@@ -1412,6 +1441,440 @@ describe('service worker host', function () {
1412
1441
  }
1413
1442
  });
1414
1443
  }); });
1444
+ /**
1445
+ * The linter is not part of the install.
1446
+ *
1447
+ * The page cannot make its first request until the worker has installed, so
1448
+ * anything the worker imports at boot is on the critical path of every
1449
+ * visit. The linter answers two routes; it is fetched when one of them is
1450
+ * called.
1451
+ */
1452
+ it('fetches the linter on the first code route, not at boot', function () { return __awaiter(void 0, void 0, void 0, function () {
1453
+ var worker, response;
1454
+ return __generator(this, function (_a) {
1455
+ switch (_a.label) {
1456
+ case 0:
1457
+ worker = seeded();
1458
+ expect(worker.imported.some(function (path) { return path.includes('lint-browser'); })).toBe(false);
1459
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/lint', runRequest({ code: 'data a;\nrun;\n' }))];
1460
+ case 1:
1461
+ response = _a.sent();
1462
+ expect(response.status).toBe(200);
1463
+ expect(worker.imported.some(function (path) { return path.includes('lint-browser'); })).toBe(true);
1464
+ return [2 /*return*/];
1465
+ }
1466
+ });
1467
+ }); });
1468
+ /**
1469
+ * A route that resolves a path from the drive is not left with a hole.
1470
+ *
1471
+ * The rules an edited file is linted against are found by walking up from
1472
+ * the file, so the request names neither path. The read that fails says
1473
+ * which piece it needs, and the request is answered again with it - the body
1474
+ * re-read from the copy taken before the first attempt.
1475
+ */
1476
+ it('fetches the piece a route resolves from the drive, not the one it names', function () { return __awaiter(void 0, void 0, void 0, function () {
1477
+ var worker, longLine, response, _a, _b, _c;
1478
+ return __generator(this, function (_d) {
1479
+ switch (_d.label) {
1480
+ case 0:
1481
+ ;
1482
+ global.__sasjsShards = {
1483
+ index: {
1484
+ format: mockSite_1.DRIVE_FORMAT,
1485
+ sharded: true,
1486
+ boot: 0,
1487
+ entries: {
1488
+ '/sasjs/drive/files/.sasjslint': { shard: 1, bytes: 20 },
1489
+ '/sasjs/drive/files/Public/app/test/prog.sas': {
1490
+ shard: 0,
1491
+ bytes: 12
1492
+ }
1493
+ },
1494
+ shards: [
1495
+ { id: 0, file: 'host/vfs-snapshot-0.json' },
1496
+ { id: 1, file: 'host/vfs-snapshot-1.json' }
1497
+ ]
1498
+ },
1499
+ 0: { '/sasjs/drive/files/Public/app/test/prog.sas': 'data a; run;' },
1500
+ 1: {
1501
+ '/sasjs/drive/files/.sasjslint': JSON.stringify({
1502
+ maxLineLength: 200
1503
+ })
1504
+ }
1505
+ };
1506
+ global.__sasjsShardFetches = [];
1507
+ worker = loadWorker();
1508
+ return [4 /*yield*/, worker.runtime.load({})];
1509
+ case 1:
1510
+ _d.sent();
1511
+ longLine = 'x'.repeat(150);
1512
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/lint', runRequest({
1513
+ code: "data _null_;\n".concat(longLine, "\nrun;\n"),
1514
+ filePath: '/Public/app/test/prog.sas'
1515
+ }))];
1516
+ case 2:
1517
+ response = _d.sent();
1518
+ expect(response.status).toBe(200);
1519
+ // the line is over the linter's default 80 and under this drive's 200, so
1520
+ // the answer proves the rules file was read - from a piece the request
1521
+ // never named
1522
+ _a = expect;
1523
+ _c = (_b = JSON).stringify;
1524
+ return [4 /*yield*/, response.json()];
1525
+ case 3:
1526
+ // the line is over the linter's default 80 and under this drive's 200, so
1527
+ // the answer proves the rules file was read - from a piece the request
1528
+ // never named
1529
+ _a.apply(void 0, [_c.apply(_b, [_d.sent()])]).not.toContain('Line exceeds');
1530
+ expect(global.__sasjsShardFetches).toEqual([0, 1]);
1531
+ return [2 /*return*/];
1532
+ }
1533
+ });
1534
+ }); });
1535
+ /**
1536
+ * A request that needs a second piece is answered, body and all.
1537
+ *
1538
+ * The first retry fetches the piece the failed read named; a request that
1539
+ * reads two pieces one after another reaches the second retry, which fetches
1540
+ * the rest. Either way the body has to arrive again - a Request's body can
1541
+ * only be read once, so the first attempt keeps an unread copy and each
1542
+ * attempt after it is made from a copy of that.
1543
+ */
1544
+ it('replays a request that needs two pieces, with its body intact', function () { return __awaiter(void 0, void 0, void 0, function () {
1545
+ var near, far, worker, longLine, response, findings, _a, _b;
1546
+ var _c, _d, _e;
1547
+ return __generator(this, function (_f) {
1548
+ switch (_f.label) {
1549
+ case 0:
1550
+ near = '/sasjs/drive/files/Public/app/test/.sasjslint';
1551
+ far = '/sasjs/drive/files/.sasjslint';
1552
+ global.__sasjsShards = {
1553
+ index: {
1554
+ format: mockSite_1.DRIVE_FORMAT,
1555
+ sharded: true,
1556
+ boot: 0,
1557
+ entries: (_c = {},
1558
+ _c[near] = { shard: 1, bytes: 20 },
1559
+ _c[far] = { shard: 2, bytes: 20 },
1560
+ _c),
1561
+ shards: [
1562
+ { id: 0, file: 'host/vfs-snapshot-0.json' },
1563
+ { id: 1, file: 'host/vfs-snapshot-1.json' },
1564
+ { id: 2, file: 'host/vfs-snapshot-2.json' }
1565
+ ]
1566
+ },
1567
+ 0: {},
1568
+ // the nearest rules file is unreadable as a config, so the walk goes on
1569
+ // to the drive root's - a second piece, in a second shard
1570
+ 1: (_d = {}, _d[near] = 'not json', _d),
1571
+ 2: (_e = {}, _e[far] = JSON.stringify({ maxLineLength: 200 }), _e)
1572
+ };
1573
+ global.__sasjsShardFetches = [];
1574
+ worker = loadWorker();
1575
+ return [4 /*yield*/, worker.runtime.load({})];
1576
+ case 1:
1577
+ _f.sent();
1578
+ longLine = 'x'.repeat(150);
1579
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/lint', runRequest({
1580
+ code: "data _null_;\n".concat(longLine, "\nrun;\n"),
1581
+ filePath: '/Public/app/test/prog.sas'
1582
+ }))];
1583
+ case 2:
1584
+ response = _f.sent();
1585
+ expect(response.status).toBe(200);
1586
+ _b = (_a = JSON).stringify;
1587
+ return [4 /*yield*/, response.json()];
1588
+ case 3:
1589
+ findings = _b.apply(_a, [_f.sent()]);
1590
+ // the code arrived on the second attempt and the root rules applied: both
1591
+ // halves of the claim
1592
+ expect(findings).toContain('Doxygen');
1593
+ expect(findings).not.toContain('Line exceeds');
1594
+ expect(global.__sasjsShardFetches).toEqual([0, 1, 2]);
1595
+ return [2 /*return*/];
1596
+ }
1597
+ });
1598
+ }); });
1599
+ /**
1600
+ * The rules are the drive's, not the linter's defaults.
1601
+ *
1602
+ * The Studio shows a visitor the drive's `.sasjslint` and invites them to
1603
+ * edit it, so a rules file nothing read would be a displayed lie. Each case
1604
+ * below changes a rule and reads the answer back through the route the
1605
+ * editor calls, because "the resolver was passed" is not the claim - "the
1606
+ * squiggle went away" is.
1607
+ */
1608
+ describe('the drive’s .sasjslint', function () {
1609
+ var rootConfig = '/sasjs/drive/files/.sasjslint';
1610
+ /** 81 characters: over the default 80, under the 100 these cases set. */
1611
+ var longLine = 'x'.repeat(81);
1612
+ var code = "data _null_;\n".concat(longLine, "\nrun;\n");
1613
+ var tooLong = function (diagnostics) {
1614
+ return diagnostics.filter(function (diagnostic) {
1615
+ return String(diagnostic.message).startsWith('Line exceeds maximum length');
1616
+ });
1617
+ };
1618
+ var lint = function (worker, body) { return __awaiter(void 0, void 0, void 0, function () {
1619
+ var response;
1620
+ return __generator(this, function (_a) {
1621
+ switch (_a.label) {
1622
+ case 0: return [4 /*yield*/, worker.dispatch('/SASjsApi/code/lint', runRequest(body))];
1623
+ case 1:
1624
+ response = _a.sent();
1625
+ expect(response.status).toBe(200);
1626
+ return [2 /*return*/, response.json()];
1627
+ }
1628
+ });
1629
+ }); };
1630
+ it('lints against the rules file at the drive root', function () { return __awaiter(void 0, void 0, void 0, function () {
1631
+ var worker, _a, _b, _c, _d;
1632
+ var _e, _f;
1633
+ return __generator(this, function (_g) {
1634
+ switch (_g.label) {
1635
+ case 0:
1636
+ worker = seeded();
1637
+ return [4 /*yield*/, worker.runtime.load((_e = {},
1638
+ _e[rootConfig] = JSON.stringify({ maxLineLength: 100 }),
1639
+ _e[drivePath('program.sas')] = code,
1640
+ _e))];
1641
+ case 1:
1642
+ _g.sent();
1643
+ _a = expect;
1644
+ _b = tooLong;
1645
+ return [4 /*yield*/, lint(worker, { code: code, filePath: appLoc + '/program.sas' })];
1646
+ case 2:
1647
+ _a.apply(void 0, [_b.apply(void 0, [_g.sent()])]).toHaveLength(0);
1648
+ // the same buffer with the default 80 in place: the finding the rule
1649
+ // exists to report
1650
+ return [4 /*yield*/, worker.runtime.load((_f = {},
1651
+ _f[rootConfig] = JSON.stringify({ maxLineLength: 80 }),
1652
+ _f[drivePath('program.sas')] = code,
1653
+ _f))];
1654
+ case 3:
1655
+ // the same buffer with the default 80 in place: the finding the rule
1656
+ // exists to report
1657
+ _g.sent();
1658
+ _c = expect;
1659
+ _d = tooLong;
1660
+ return [4 /*yield*/, lint(worker, { code: code, filePath: appLoc + '/program.sas' })];
1661
+ case 4:
1662
+ _c.apply(void 0, [_d.apply(void 0, [_g.sent()])]).toHaveLength(1);
1663
+ return [2 /*return*/];
1664
+ }
1665
+ });
1666
+ }); });
1667
+ it('re-reads the rules per request, so an edit takes effect', function () { return __awaiter(void 0, void 0, void 0, function () {
1668
+ var worker, path, _a, _b, form, patched, _c, _d;
1669
+ var _e;
1670
+ return __generator(this, function (_f) {
1671
+ switch (_f.label) {
1672
+ case 0:
1673
+ worker = seeded();
1674
+ path = appLoc + '/program.sas';
1675
+ return [4 /*yield*/, worker.runtime.load((_e = {},
1676
+ _e[rootConfig] = JSON.stringify({ maxLineLength: 80 }),
1677
+ _e[drivePath('program.sas')] = code,
1678
+ _e))];
1679
+ case 1:
1680
+ _f.sent();
1681
+ _a = expect;
1682
+ _b = tooLong;
1683
+ return [4 /*yield*/, lint(worker, { code: code, filePath: path })];
1684
+ case 2:
1685
+ _a.apply(void 0, [_b.apply(void 0, [_f.sent()])]).toHaveLength(1);
1686
+ form = new FormData();
1687
+ form.set('file', new Blob([JSON.stringify({ maxLineLength: 100 })], {
1688
+ type: 'application/json'
1689
+ }), '.sasjslint');
1690
+ return [4 /*yield*/, worker.dispatch("/SASjsApi/drive/file?_filePath=".concat(encodeURIComponent('/.sasjslint')), { method: 'PATCH', body: form })];
1691
+ case 3:
1692
+ patched = _f.sent();
1693
+ expect(patched.status).toBe(200);
1694
+ _c = expect;
1695
+ _d = tooLong;
1696
+ return [4 /*yield*/, lint(worker, { code: code, filePath: path })];
1697
+ case 4:
1698
+ _c.apply(void 0, [_d.apply(void 0, [_f.sent()])]).toHaveLength(0);
1699
+ return [2 /*return*/];
1700
+ }
1701
+ });
1702
+ }); });
1703
+ it('gives a folder its own rules for everything beneath it', function () { return __awaiter(void 0, void 0, void 0, function () {
1704
+ var worker, _a, _b, _c, _d;
1705
+ var _e;
1706
+ return __generator(this, function (_f) {
1707
+ switch (_f.label) {
1708
+ case 0:
1709
+ worker = seeded();
1710
+ return [4 /*yield*/, worker.runtime.load((_e = {},
1711
+ _e[rootConfig] = JSON.stringify({ maxLineLength: 80 }),
1712
+ // the nearer file relaxes the rule for this folder alone
1713
+ _e["/sasjs/drive/files".concat(appLoc, "/.sasjslint")] = JSON.stringify({
1714
+ maxLineLength: 200
1715
+ }),
1716
+ _e))];
1717
+ case 1:
1718
+ _f.sent();
1719
+ _a = expect;
1720
+ _b = tooLong;
1721
+ return [4 /*yield*/, lint(worker, {
1722
+ code: code,
1723
+ filePath: "".concat(appLoc, "/services/public/program.sas")
1724
+ })];
1725
+ case 2:
1726
+ _a.apply(void 0, [_b.apply(void 0, [_f.sent()])]).toHaveLength(0);
1727
+ // outside that folder the root file still applies
1728
+ _c = expect;
1729
+ _d = tooLong;
1730
+ return [4 /*yield*/, lint(worker, {
1731
+ code: code,
1732
+ filePath: '/home/sasjsadmin/program.sas'
1733
+ })];
1734
+ case 3:
1735
+ // outside that folder the root file still applies
1736
+ _c.apply(void 0, [_d.apply(void 0, [_f.sent()])]).toHaveLength(1);
1737
+ return [2 /*return*/];
1738
+ }
1739
+ });
1740
+ }); });
1741
+ it('finds the rules for a path spelled the way a Windows client sends it', function () { return __awaiter(void 0, void 0, void 0, function () {
1742
+ var worker, _a, _b;
1743
+ var _c;
1744
+ return __generator(this, function (_d) {
1745
+ switch (_d.label) {
1746
+ case 0:
1747
+ worker = seeded();
1748
+ return [4 /*yield*/, worker.runtime.load((_c = {},
1749
+ _c["/sasjs/drive/files".concat(appLoc, "/.sasjslint")] = JSON.stringify({
1750
+ maxLineLength: 100
1751
+ }),
1752
+ _c))];
1753
+ case 1:
1754
+ _d.sent();
1755
+ _a = expect;
1756
+ _b = tooLong;
1757
+ return [4 /*yield*/, lint(worker, {
1758
+ code: code,
1759
+ filePath: "".concat(appLoc, "\\services\\public\\program.sas")
1760
+ })];
1761
+ case 2:
1762
+ _a.apply(void 0, [_b.apply(void 0, [_d.sent()])]).toHaveLength(0);
1763
+ return [2 /*return*/];
1764
+ }
1765
+ });
1766
+ }); });
1767
+ it('skips a rules file that does not parse and uses the one above', function () { return __awaiter(void 0, void 0, void 0, function () {
1768
+ var worker, _a, _b;
1769
+ var _c;
1770
+ return __generator(this, function (_d) {
1771
+ switch (_d.label) {
1772
+ case 0:
1773
+ worker = seeded();
1774
+ return [4 /*yield*/, worker.runtime.load((_c = {},
1775
+ _c[rootConfig] = JSON.stringify({ maxLineLength: 100 }),
1776
+ _c["/sasjs/drive/files".concat(appLoc, "/.sasjslint")] = '{ this is not json',
1777
+ _c))
1778
+ // a broken near file must not break linting, and must not silently
1779
+ // replace the rules that were working
1780
+ ];
1781
+ case 1:
1782
+ _d.sent();
1783
+ // a broken near file must not break linting, and must not silently
1784
+ // replace the rules that were working
1785
+ _a = expect;
1786
+ _b = tooLong;
1787
+ return [4 /*yield*/, lint(worker, {
1788
+ code: code,
1789
+ filePath: "".concat(appLoc, "/services/public/program.sas")
1790
+ })];
1791
+ case 2:
1792
+ // a broken near file must not break linting, and must not silently
1793
+ // replace the rules that were working
1794
+ _a.apply(void 0, [_b.apply(void 0, [_d.sent()])]).toHaveLength(0);
1795
+ return [2 /*return*/];
1796
+ }
1797
+ });
1798
+ }); });
1799
+ it('takes the drive root rules for a buffer with no file, and for a path that escapes the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
1800
+ var worker, _a, _b, _c, _d;
1801
+ var _e;
1802
+ return __generator(this, function (_f) {
1803
+ switch (_f.label) {
1804
+ case 0:
1805
+ worker = seeded();
1806
+ return [4 /*yield*/, worker.runtime.load((_e = {},
1807
+ _e[rootConfig] = JSON.stringify({ maxLineLength: 100 }),
1808
+ _e))];
1809
+ case 1:
1810
+ _f.sent();
1811
+ _a = expect;
1812
+ _b = tooLong;
1813
+ return [4 /*yield*/, lint(worker, { code: code })];
1814
+ case 2:
1815
+ _a.apply(void 0, [_b.apply(void 0, [_f.sent()])]).toHaveLength(0);
1816
+ _c = expect;
1817
+ _d = tooLong;
1818
+ return [4 /*yield*/, lint(worker, {
1819
+ code: code,
1820
+ filePath: '../../outside/program.sas'
1821
+ })];
1822
+ case 3:
1823
+ _c.apply(void 0, [_d.apply(void 0, [_f.sent()])]).toHaveLength(0);
1824
+ return [2 /*return*/];
1825
+ }
1826
+ });
1827
+ }); });
1828
+ it('falls back to the linter defaults when the drive has no rules file', function () { return __awaiter(void 0, void 0, void 0, function () {
1829
+ var worker, _a, _b;
1830
+ var _c;
1831
+ return __generator(this, function (_d) {
1832
+ switch (_d.label) {
1833
+ case 0:
1834
+ worker = seeded();
1835
+ return [4 /*yield*/, worker.runtime.load((_c = {}, _c[drivePath('program.sas')] = code, _c))];
1836
+ case 1:
1837
+ _d.sent();
1838
+ _a = expect;
1839
+ _b = tooLong;
1840
+ return [4 /*yield*/, lint(worker, {
1841
+ code: code,
1842
+ filePath: "".concat(appLoc, "/program.sas")
1843
+ })];
1844
+ case 2:
1845
+ _a.apply(void 0, [_b.apply(void 0, [_d.sent()])]).toHaveLength(1);
1846
+ return [2 /*return*/];
1847
+ }
1848
+ });
1849
+ }); });
1850
+ it('formats with the rules file too', function () { return __awaiter(void 0, void 0, void 0, function () {
1851
+ var worker, formatted, _a;
1852
+ var _b;
1853
+ return __generator(this, function (_c) {
1854
+ switch (_c.label) {
1855
+ case 0:
1856
+ worker = seeded();
1857
+ return [4 /*yield*/, worker.runtime.load((_b = {},
1858
+ _b[rootConfig] = JSON.stringify({
1859
+ hasDoxygenHeader: true,
1860
+ defaultHeader: '/**\n @brief the drive header\n**/'
1861
+ }),
1862
+ _b))];
1863
+ case 1:
1864
+ _c.sent();
1865
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/format', runRequest({ code: code, filePath: '/home/sasjsadmin/program.sas' }))];
1866
+ case 2:
1867
+ formatted = _c.sent();
1868
+ expect(formatted.status).toBe(200);
1869
+ _a = expect;
1870
+ return [4 /*yield*/, formatted.text()];
1871
+ case 3:
1872
+ _a.apply(void 0, [_c.sent()]).toContain('@brief the drive header');
1873
+ return [2 /*return*/];
1874
+ }
1875
+ });
1876
+ }); });
1877
+ });
1415
1878
  it('keeps an app’s URL and tiles inside the folder the site is served from', function () { return __awaiter(void 0, void 0, void 0, function () {
1416
1879
  var worker, archive, form, published, app, listing, apps, landing, _a, page, _b;
1417
1880
  return __generator(this, function (_c) {
@@ -1985,6 +2448,119 @@ describe('service worker host', function () {
1985
2448
  }
1986
2449
  });
1987
2450
  }); });
2451
+ /**
2452
+ * A sharded drive supplies the piece a request names, and only that one.
2453
+ *
2454
+ * The build splits a captured drive so the first page does not download every
2455
+ * app's bundle. That makes it the host's job to fetch the piece holding the
2456
+ * path a route reads - here a drive file, named in `_filePath` - and to leave
2457
+ * the pieces nobody asked for on the site.
2458
+ */
2459
+ it('fetches the piece a drive request names, and leaves the rest', function () { return __awaiter(void 0, void 0, void 0, function () {
2460
+ var small, large, worker, response, _a;
2461
+ var _b, _c, _d;
2462
+ return __generator(this, function (_e) {
2463
+ switch (_e.label) {
2464
+ case 0:
2465
+ small = '/sasjs/drive/files/config.json';
2466
+ large = '/sasjs/drive/files/data/large.csv';
2467
+ global.__sasjsShards = {
2468
+ index: {
2469
+ format: mockSite_1.DRIVE_FORMAT,
2470
+ sharded: true,
2471
+ boot: 0,
2472
+ entries: (_b = {},
2473
+ _b[small] = { shard: 0, bytes: 2 },
2474
+ _b[large] = { shard: 1, bytes: 64 },
2475
+ _b),
2476
+ shards: [
2477
+ { id: 0, file: 'host/vfs-snapshot-0.json' },
2478
+ { id: 1, file: 'host/vfs-snapshot-1.json' }
2479
+ ]
2480
+ },
2481
+ 0: (_c = {}, _c[small] = '{}', _c),
2482
+ 1: (_d = {}, _d[large] = 'a,b\n1,2\n3,4\n', _d)
2483
+ };
2484
+ global.__sasjsShardFetches = [];
2485
+ worker = loadWorker();
2486
+ return [4 /*yield*/, worker.runtime.load({})
2487
+ // the boot shard arrived with the drive; the second piece did not
2488
+ ];
2489
+ case 1:
2490
+ _e.sent();
2491
+ // the boot shard arrived with the drive; the second piece did not
2492
+ expect(global.__sasjsShardFetches).toEqual([0]);
2493
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/large.csv'))];
2494
+ case 2:
2495
+ response = _e.sent();
2496
+ expect(response.status).toBe(200);
2497
+ _a = expect;
2498
+ return [4 /*yield*/, response.text()];
2499
+ case 3:
2500
+ _a.apply(void 0, [_e.sent()]).toContain('a,b');
2501
+ expect(global.__sasjsShardFetches).toEqual([0, 1]);
2502
+ // a second read of the same piece is answered from memory
2503
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/large.csv'))];
2504
+ case 4:
2505
+ // a second read of the same piece is answered from memory
2506
+ _e.sent();
2507
+ expect(global.__sasjsShardFetches).toEqual([0, 1]);
2508
+ return [2 /*return*/];
2509
+ }
2510
+ });
2511
+ }); });
2512
+ /**
2513
+ * An export is the whole drive, not the part in memory.
2514
+ *
2515
+ * `window.SASjsMock.download()` writes the file a project ships as
2516
+ * `mock-snapshot.json`, so a sharded drive must be fetched before it is
2517
+ * dumped - otherwise the next build ships a snapshot that has quietly lost
2518
+ * every app the visitor never opened.
2519
+ */
2520
+ it('exports every piece of a sharded drive, not only the loaded ones', function () { return __awaiter(void 0, void 0, void 0, function () {
2521
+ var small, large, worker, _a, replies, pending, exported;
2522
+ var _b, _c, _d;
2523
+ return __generator(this, function (_e) {
2524
+ switch (_e.label) {
2525
+ case 0:
2526
+ small = '/sasjs/drive/files/config.json';
2527
+ large = '/sasjs/drive/files/data/large.csv';
2528
+ global.__sasjsShards = {
2529
+ index: {
2530
+ format: mockSite_1.DRIVE_FORMAT,
2531
+ sharded: true,
2532
+ boot: 0,
2533
+ entries: (_b = {},
2534
+ _b[small] = { shard: 0, bytes: 2 },
2535
+ _b[large] = { shard: 1, bytes: 8 },
2536
+ _b),
2537
+ shards: [
2538
+ { id: 0, file: 'host/vfs-snapshot-0.json' },
2539
+ { id: 1, file: 'host/vfs-snapshot-1.json' }
2540
+ ]
2541
+ },
2542
+ 0: (_c = {}, _c[small] = '{}', _c),
2543
+ 1: (_d = {}, _d[large] = 'a,b\n1,2\n', _d)
2544
+ };
2545
+ global.__sasjsShardFetches = [];
2546
+ worker = loadWorker();
2547
+ return [4 /*yield*/, worker.runtime.load({})];
2548
+ case 1:
2549
+ _e.sent();
2550
+ _a = worker.message({ type: 'sasjs-snapshot' }), replies = _a.replies, pending = _a.pending;
2551
+ return [4 /*yield*/, pending
2552
+ // the piece nobody read was fetched for the export
2553
+ ];
2554
+ case 2:
2555
+ _e.sent();
2556
+ // the piece nobody read was fetched for the export
2557
+ expect(global.__sasjsShardFetches).toEqual([0, 1]);
2558
+ exported = replies.find(function (reply) { return reply.type === 'sasjs-snapshot'; });
2559
+ expect(Object.keys(exported.files).sort()).toEqual([large, small].sort());
2560
+ return [2 /*return*/];
2561
+ }
2562
+ });
2563
+ }); });
1988
2564
  it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
1989
2565
  var dispatch, response, _a;
1990
2566
  return __generator(this, function (_b) {
@@ -2556,16 +3132,23 @@ describe('service worker host', function () {
2556
3132
  expect(seed.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-seed-ack' }));
2557
3133
  runtime.load(files);
2558
3134
  snapshot = message({ type: 'sasjs-snapshot' });
3135
+ // the reply waits for the drive to be whole, so a sharded site's export is
3136
+ // not the part of it that happens to be in memory
3137
+ return [4 /*yield*/, snapshot.pending];
3138
+ case 3:
3139
+ // the reply waits for the drive to be whole, so a sharded site's export is
3140
+ // not the part of it that happens to be in memory
3141
+ _c.sent();
2559
3142
  expect(snapshot.replies[0].type).toBe('sasjs-snapshot');
2560
3143
  expect(Object.keys(snapshot.replies[0].files).length).toBe(1);
2561
3144
  reset = message({ type: 'sasjs-reset' });
2562
3145
  return [4 /*yield*/, reset.pending];
2563
- case 3:
3146
+ case 4:
2564
3147
  _c.sent();
2565
3148
  expect(reset.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-reset-ack' }));
2566
3149
  stats = message({ type: 'sasjs-stats' });
2567
3150
  return [4 /*yield*/, stats.pending];
2568
- case 4:
3151
+ case 5:
2569
3152
  _c.sent();
2570
3153
  expect(stats.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-stats' }));
2571
3154
  return [2 /*return*/];