@sasjs/cli 6.0.0 → 6.2.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.
- package/build/commands/build/build.js +20 -10
- package/build/commands/build/internal/mockSite/assets/intercept.js +172 -0
- package/build/commands/build/internal/mockSite/assets/md5.js +75 -0
- package/build/commands/build/internal/mockSite/assets/persist.js +222 -0
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +1230 -0
- package/build/commands/build/internal/mockSite/assets/sw.js +317 -0
- package/build/commands/build/internal/mockSite/mockSite.d.ts +38 -0
- package/build/commands/build/internal/mockSite/mockSite.js +475 -0
- package/build/commands/build/internal/mockSite/spec/intercept.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/intercept.spec.js +267 -0
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +497 -0
- package/build/commands/build/internal/mockSite/spec/persist.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +476 -0
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +802 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +716 -0
- package/build/commands/index.d.ts +1 -0
- package/build/commands/index.js +3 -1
- package/package.json +8 -7
|
@@ -100,12 +100,13 @@ var compile_1 = require("../compile/compile");
|
|
|
100
100
|
var internal_1 = require("../compile/internal");
|
|
101
101
|
var config_2 = require("./internal/config");
|
|
102
102
|
var getLaunchPageCode_1 = require("./internal/getLaunchPageCode");
|
|
103
|
+
var mockSite_1 = require("./internal/mockSite/mockSite");
|
|
103
104
|
function build(target) {
|
|
104
105
|
return __awaiter(this, void 0, void 0, function () {
|
|
105
106
|
var macroCorePath, _a;
|
|
106
|
-
var _b;
|
|
107
|
-
return __generator(this, function (
|
|
108
|
-
switch (
|
|
107
|
+
var _b, _c;
|
|
108
|
+
return __generator(this, function (_d) {
|
|
109
|
+
switch (_d.label) {
|
|
109
110
|
case 0:
|
|
110
111
|
macroCorePath = process.sasjsConstants.macroCorePath;
|
|
111
112
|
if (macroCorePath === '') {
|
|
@@ -120,18 +121,27 @@ function build(target) {
|
|
|
120
121
|
if (!_a) return [3 /*break*/, 2];
|
|
121
122
|
return [4 /*yield*/, (0, utils_1.folderExists)(process.sasjsConstants.buildDestinationFolder)];
|
|
122
123
|
case 1:
|
|
123
|
-
_a = !(
|
|
124
|
-
|
|
124
|
+
_a = !(_d.sent());
|
|
125
|
+
_d.label = 2;
|
|
125
126
|
case 2:
|
|
126
127
|
if (!_a) return [3 /*break*/, 4];
|
|
127
128
|
return [4 /*yield*/, (0, compile_1.compile)(target)];
|
|
128
129
|
case 3:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
case 4: return [4 /*yield*/, createFinalSasFiles(target)
|
|
130
|
+
_d.sent();
|
|
131
|
+
_d.label = 4;
|
|
132
|
+
case 4: return [4 /*yield*/, createFinalSasFiles(target)
|
|
133
|
+
// streamConfig.mockWeb opts the target into a static site that runs the same
|
|
134
|
+
// services in a browser. It is built from what compile just produced, so it
|
|
135
|
+
// is only meaningful after the tree above exists.
|
|
136
|
+
];
|
|
132
137
|
case 5:
|
|
133
|
-
|
|
134
|
-
return [
|
|
138
|
+
_d.sent();
|
|
139
|
+
if (!((_c = target.streamConfig) === null || _c === void 0 ? void 0 : _c.mockWeb)) return [3 /*break*/, 7];
|
|
140
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target)];
|
|
141
|
+
case 6:
|
|
142
|
+
_d.sent();
|
|
143
|
+
_d.label = 7;
|
|
144
|
+
case 7: return [2 /*return*/];
|
|
135
145
|
}
|
|
136
146
|
});
|
|
137
147
|
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-side bridge for the browser build.
|
|
3
|
+
*
|
|
4
|
+
* The real work happens in the service worker (see /sw.js), so every request
|
|
5
|
+
* still goes over the network layer and is visible in DevTools > Network as
|
|
6
|
+
* "(from ServiceWorker)" - request payload and response body included.
|
|
7
|
+
*
|
|
8
|
+
* Two things are this file's job:
|
|
9
|
+
* 1. XHR bodies are multipart (the adapter always sends them that way).
|
|
10
|
+
* Reading multipart inside a worker is unreliable for XHR-originated
|
|
11
|
+
* requests, so the payload is converted to JSON here, where reading a
|
|
12
|
+
* FormData is trivial.
|
|
13
|
+
* 2. The XHR object the app is holding must behave like a normal one: status,
|
|
14
|
+
* readyState, responseText and real load events.
|
|
15
|
+
*/
|
|
16
|
+
;(function () {
|
|
17
|
+
if (window.__sasjsMockBridgeInstalled) return
|
|
18
|
+
window.__sasjsMockBridgeInstalled = true
|
|
19
|
+
|
|
20
|
+
// Set localStorage sasjsMockDebug='1' and reload to see this bridge's traffic
|
|
21
|
+
// in the console.
|
|
22
|
+
const DEBUG = (() => {
|
|
23
|
+
try {
|
|
24
|
+
return localStorage.getItem('sasjsMockDebug') === '1'
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
})()
|
|
29
|
+
|
|
30
|
+
const dbg = (...args) => {
|
|
31
|
+
if (DEBUG) console.log('[sasjs-mock]', ...args)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const readBody = async (body) => {
|
|
35
|
+
const params = {}
|
|
36
|
+
const tables = []
|
|
37
|
+
if (!body) return { params, tables }
|
|
38
|
+
|
|
39
|
+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
40
|
+
for (const [key, value] of body.entries()) {
|
|
41
|
+
if (typeof value === 'string') params[key] = value
|
|
42
|
+
else
|
|
43
|
+
tables.push({
|
|
44
|
+
name: key,
|
|
45
|
+
filename: value.name || key + '.csv',
|
|
46
|
+
csv: await value.text()
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
return { params, tables }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof body === 'string') {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(body)
|
|
55
|
+
return { params: {}, tables: [], json: parsed }
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return { params: {}, tables: [] }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { params, tables }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const safeParse = (text) => {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(text)
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return text
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const respond = (xhr, status, contentType, body) => {
|
|
72
|
+
Object.defineProperty(xhr, 'status', { value: status, configurable: true })
|
|
73
|
+
Object.defineProperty(xhr, 'statusText', {
|
|
74
|
+
value: status === 200 ? 'OK' : 'ERROR',
|
|
75
|
+
configurable: true
|
|
76
|
+
})
|
|
77
|
+
Object.defineProperty(xhr, 'readyState', { value: 4, configurable: true })
|
|
78
|
+
Object.defineProperty(xhr, 'responseText', {
|
|
79
|
+
value: body,
|
|
80
|
+
configurable: true
|
|
81
|
+
})
|
|
82
|
+
Object.defineProperty(xhr, 'responseURL', {
|
|
83
|
+
value: xhr.__sasjsMockUrl,
|
|
84
|
+
configurable: true
|
|
85
|
+
})
|
|
86
|
+
Object.defineProperty(xhr, 'response', {
|
|
87
|
+
value: xhr.responseType === 'json' ? safeParse(body) : body,
|
|
88
|
+
configurable: true
|
|
89
|
+
})
|
|
90
|
+
xhr.getAllResponseHeaders = () => 'content-type: ' + contentType + '\r\n'
|
|
91
|
+
xhr.getResponseHeader = (key) =>
|
|
92
|
+
key && key.toLowerCase() === 'content-type' ? contentType : null
|
|
93
|
+
|
|
94
|
+
xhr.dispatchEvent(new Event('readystatechange'))
|
|
95
|
+
xhr.dispatchEvent(new ProgressEvent('load'))
|
|
96
|
+
xhr.dispatchEvent(new ProgressEvent('loadend'))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isSasjs = (url) =>
|
|
100
|
+
typeof url === 'string' && url.indexOf('/SASjsApi/') >= 0
|
|
101
|
+
|
|
102
|
+
const proto = XMLHttpRequest.prototype
|
|
103
|
+
const nativeOpen = proto.open
|
|
104
|
+
const nativeSend = proto.send
|
|
105
|
+
|
|
106
|
+
proto.open = function (method, url) {
|
|
107
|
+
this.__sasjsMockMethod = method
|
|
108
|
+
this.__sasjsMockUrl = String(url)
|
|
109
|
+
return nativeOpen.apply(this, arguments)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
proto.send = function (body) {
|
|
113
|
+
if (!isSasjs(this.__sasjsMockUrl)) return nativeSend.apply(this, arguments)
|
|
114
|
+
|
|
115
|
+
const xhr = this
|
|
116
|
+
const method = (this.__sasjsMockMethod || 'GET').toUpperCase()
|
|
117
|
+
|
|
118
|
+
readBody(body)
|
|
119
|
+
.then(async (payload) => {
|
|
120
|
+
const init = {
|
|
121
|
+
method,
|
|
122
|
+
credentials: 'same-origin',
|
|
123
|
+
headers: { accept: '*/*' }
|
|
124
|
+
}
|
|
125
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
126
|
+
init.headers['content-type'] = 'application/json'
|
|
127
|
+
init.body = JSON.stringify(payload)
|
|
128
|
+
}
|
|
129
|
+
dbg(
|
|
130
|
+
method,
|
|
131
|
+
xhr.__sasjsMockUrl,
|
|
132
|
+
payload.tables.map((t) => t.name).join('|')
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const res = await fetch(xhr.__sasjsMockUrl, init)
|
|
136
|
+
const text = await res.text()
|
|
137
|
+
const contentType = res.headers.get('content-type') || 'text/plain'
|
|
138
|
+
if (res.status >= 400)
|
|
139
|
+
console.error(
|
|
140
|
+
'[sasjs-mock] ' +
|
|
141
|
+
xhr.__sasjsMockUrl +
|
|
142
|
+
' -> ' +
|
|
143
|
+
res.status +
|
|
144
|
+
' ' +
|
|
145
|
+
text.slice(0, 400)
|
|
146
|
+
)
|
|
147
|
+
respond(xhr, res.status, contentType, text)
|
|
148
|
+
})
|
|
149
|
+
.catch((err) => {
|
|
150
|
+
const message = String((err && err.message) || err)
|
|
151
|
+
console.error(
|
|
152
|
+
'[sasjs-mock] request failed: ' + xhr.__sasjsMockUrl + ' - ' + message
|
|
153
|
+
)
|
|
154
|
+
respond(
|
|
155
|
+
xhr,
|
|
156
|
+
500,
|
|
157
|
+
'application/json',
|
|
158
|
+
JSON.stringify({ message: message, error: message })
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// fetch() is used by nothing in this build, but answering it keeps the
|
|
164
|
+
// bridge honest if a code path switches transport.
|
|
165
|
+
window.__sasjsMockHostStats = async () => {
|
|
166
|
+
const reg = await navigator.serviceWorker.ready
|
|
167
|
+
return {
|
|
168
|
+
active: !!reg.active,
|
|
169
|
+
controller: !!navigator.serviceWorker.controller
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
})()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Compact MD5 (RFC 1321) returning lowercase hex. Used by the crypto shim so
|
|
2
|
+
// that dcMockUtils' mp_hashdataset sees the same digests SAS produces.
|
|
3
|
+
const MD5 = (() => {
|
|
4
|
+
const S = [
|
|
5
|
+
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5,
|
|
6
|
+
9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11,
|
|
7
|
+
16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10,
|
|
8
|
+
15, 21
|
|
9
|
+
]
|
|
10
|
+
const K = new Array(64)
|
|
11
|
+
for (let i = 0; i < 64; i++)
|
|
12
|
+
K[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 4294967296) | 0
|
|
13
|
+
|
|
14
|
+
const rotl = (x, c) => (x << c) | (x >>> (32 - c))
|
|
15
|
+
|
|
16
|
+
return function md5(input) {
|
|
17
|
+
const bytes = new TextEncoder().encode(String(input))
|
|
18
|
+
const bitLen = bytes.length * 8
|
|
19
|
+
const withPad = new Uint8Array((((bytes.length + 8) >> 6) + 1) * 64)
|
|
20
|
+
withPad.set(bytes)
|
|
21
|
+
withPad[bytes.length] = 0x80
|
|
22
|
+
const dv = new DataView(withPad.buffer)
|
|
23
|
+
dv.setUint32(withPad.length - 8, bitLen >>> 0, true)
|
|
24
|
+
dv.setUint32(withPad.length - 4, Math.floor(bitLen / 4294967296), true)
|
|
25
|
+
|
|
26
|
+
let a0 = 0x67452301,
|
|
27
|
+
b0 = 0xefcdab89,
|
|
28
|
+
c0 = 0x98badcfe,
|
|
29
|
+
d0 = 0x10325476
|
|
30
|
+
|
|
31
|
+
for (let off = 0; off < withPad.length; off += 64) {
|
|
32
|
+
const M = new Array(16)
|
|
33
|
+
for (let i = 0; i < 16; i++) M[i] = dv.getInt32(off + i * 4, true)
|
|
34
|
+
let A = a0,
|
|
35
|
+
B = b0,
|
|
36
|
+
C = c0,
|
|
37
|
+
D = d0
|
|
38
|
+
for (let i = 0; i < 64; i++) {
|
|
39
|
+
let F, g
|
|
40
|
+
if (i < 16) {
|
|
41
|
+
F = (B & C) | (~B & D)
|
|
42
|
+
g = i
|
|
43
|
+
} else if (i < 32) {
|
|
44
|
+
F = (D & B) | (~D & C)
|
|
45
|
+
g = (5 * i + 1) % 16
|
|
46
|
+
} else if (i < 48) {
|
|
47
|
+
F = B ^ C ^ D
|
|
48
|
+
g = (3 * i + 5) % 16
|
|
49
|
+
} else {
|
|
50
|
+
F = C ^ (B | ~D)
|
|
51
|
+
g = (7 * i) % 16
|
|
52
|
+
}
|
|
53
|
+
const tmp = D
|
|
54
|
+
D = C
|
|
55
|
+
C = B
|
|
56
|
+
B = (B + rotl((A + F + K[i] + M[g]) | 0, S[i])) | 0
|
|
57
|
+
A = tmp
|
|
58
|
+
}
|
|
59
|
+
a0 = (a0 + A) | 0
|
|
60
|
+
b0 = (b0 + B) | 0
|
|
61
|
+
c0 = (c0 + C) | 0
|
|
62
|
+
d0 = (d0 + D) | 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const hex = (n) => {
|
|
66
|
+
let out = ''
|
|
67
|
+
for (let i = 0; i < 4; i++)
|
|
68
|
+
out += ((n >>> (i * 8)) & 0xff).toString(16).padStart(2, '0')
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
return hex(a0) + hex(b0) + hex(c0) + hex(d0)
|
|
72
|
+
}
|
|
73
|
+
})()
|
|
74
|
+
|
|
75
|
+
if (typeof module !== 'undefined') module.exports = MD5
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive storage for the browser build: IndexedDB, owned by the page.
|
|
3
|
+
*
|
|
4
|
+
* The service worker hosts the runtime but may be terminated between requests,
|
|
5
|
+
* so it holds no durable state. On boot this script reads the persisted drive
|
|
6
|
+
* and hands it to the worker; after every service run the worker sends back the
|
|
7
|
+
* files it changed and they are written here.
|
|
8
|
+
*/
|
|
9
|
+
;(function () {
|
|
10
|
+
const DB_NAME = 'sasjs-mock-host'
|
|
11
|
+
const STORE = 'files'
|
|
12
|
+
|
|
13
|
+
const openDb = () =>
|
|
14
|
+
new Promise((resolve, reject) => {
|
|
15
|
+
const req = indexedDB.open(DB_NAME, 1)
|
|
16
|
+
req.onupgradeneeded = () => {
|
|
17
|
+
const db = req.result
|
|
18
|
+
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE)
|
|
19
|
+
}
|
|
20
|
+
req.onsuccess = () => resolve(req.result)
|
|
21
|
+
req.onerror = () => reject(req.error)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const withStore = async (mode, run) => {
|
|
25
|
+
const db = await openDb()
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const tx = db.transaction(STORE, mode)
|
|
28
|
+
const store = tx.objectStore(STORE)
|
|
29
|
+
let result
|
|
30
|
+
try {
|
|
31
|
+
result = run(store)
|
|
32
|
+
} catch (e) {
|
|
33
|
+
reject(e)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
tx.oncomplete = () => resolve(result)
|
|
37
|
+
tx.onerror = () => reject(tx.error)
|
|
38
|
+
tx.onabort = () => reject(tx.error)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const readAll = () =>
|
|
43
|
+
new Promise(async (resolve, reject) => {
|
|
44
|
+
try {
|
|
45
|
+
const db = await openDb()
|
|
46
|
+
const keysReq = db
|
|
47
|
+
.transaction(STORE, 'readonly')
|
|
48
|
+
.objectStore(STORE)
|
|
49
|
+
.getAllKeys()
|
|
50
|
+
keysReq.onsuccess = () => {
|
|
51
|
+
const keys = keysReq.result
|
|
52
|
+
if (!keys.length) return resolve({})
|
|
53
|
+
const valsReq = db
|
|
54
|
+
.transaction(STORE, 'readonly')
|
|
55
|
+
.objectStore(STORE)
|
|
56
|
+
.getAll()
|
|
57
|
+
valsReq.onsuccess = () => {
|
|
58
|
+
const files = {}
|
|
59
|
+
keys.forEach((k, i) => {
|
|
60
|
+
if (typeof valsReq.result[i] === 'string')
|
|
61
|
+
files[k] = valsReq.result[i]
|
|
62
|
+
})
|
|
63
|
+
resolve(files)
|
|
64
|
+
}
|
|
65
|
+
valsReq.onerror = () => reject(valsReq.error)
|
|
66
|
+
}
|
|
67
|
+
keysReq.onerror = () => reject(keysReq.error)
|
|
68
|
+
} catch (e) {
|
|
69
|
+
reject(e)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const writeFiles = async (files) => {
|
|
74
|
+
const entries = Object.entries(files || {})
|
|
75
|
+
if (!entries.length) return { written: 0, deleted: 0 }
|
|
76
|
+
let deleted = 0
|
|
77
|
+
await withStore('readwrite', (store) => {
|
|
78
|
+
for (const [path, contents] of entries) {
|
|
79
|
+
if (contents === null || contents === undefined) {
|
|
80
|
+
store.delete(path)
|
|
81
|
+
deleted++
|
|
82
|
+
} else {
|
|
83
|
+
store.put(contents, path)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
return { written: entries.length - deleted, deleted }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const clearAll = () => withStore('readwrite', (store) => store.clear())
|
|
91
|
+
|
|
92
|
+
const waitForController = () =>
|
|
93
|
+
new Promise((resolve) => {
|
|
94
|
+
if (navigator.serviceWorker.controller)
|
|
95
|
+
return resolve(navigator.serviceWorker.controller)
|
|
96
|
+
let waited = 0
|
|
97
|
+
const tick = () => {
|
|
98
|
+
if (navigator.serviceWorker.controller)
|
|
99
|
+
return resolve(navigator.serviceWorker.controller)
|
|
100
|
+
if (waited > 10000) return resolve(null)
|
|
101
|
+
waited += 200
|
|
102
|
+
setTimeout(tick, 200)
|
|
103
|
+
}
|
|
104
|
+
tick()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const askWorker = (message, expect) =>
|
|
108
|
+
new Promise((resolve) => {
|
|
109
|
+
const sw = navigator.serviceWorker
|
|
110
|
+
let settled = false
|
|
111
|
+
let timer
|
|
112
|
+
const finish = (value) => {
|
|
113
|
+
if (settled) return
|
|
114
|
+
settled = true
|
|
115
|
+
clearTimeout(timer)
|
|
116
|
+
sw.removeEventListener('message', handler)
|
|
117
|
+
resolve(value)
|
|
118
|
+
}
|
|
119
|
+
const handler = (ev) => {
|
|
120
|
+
if (ev.data && ev.data.type === expect) finish(ev.data)
|
|
121
|
+
}
|
|
122
|
+
// The listener is removed on every path: a page that asks repeatedly with
|
|
123
|
+
// a slow worker would otherwise stack one listener per call, and a reply
|
|
124
|
+
// arriving after the timeout would land on a listener nobody reads.
|
|
125
|
+
timer = setTimeout(
|
|
126
|
+
() => finish({ error: 'timed out waiting for ' + expect }),
|
|
127
|
+
20000
|
|
128
|
+
)
|
|
129
|
+
sw.addEventListener('message', handler)
|
|
130
|
+
waitForController().then((controller) => {
|
|
131
|
+
if (!controller)
|
|
132
|
+
return finish({ error: 'no service worker controller' })
|
|
133
|
+
controller.postMessage(message)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Persist whatever the worker reports as changed.
|
|
138
|
+
navigator.serviceWorker.addEventListener('message', (ev) => {
|
|
139
|
+
const data = ev.data || {}
|
|
140
|
+
if (data.type === 'sasjs-persist')
|
|
141
|
+
writeFiles(data.files).catch((e) =>
|
|
142
|
+
console.error('[sasjs-mock] persist failed', e)
|
|
143
|
+
)
|
|
144
|
+
if (data.type === 'sasjs-host-error')
|
|
145
|
+
console.error('[sasjs-mock] ' + data.path + ': ' + data.message)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
window.SASjsMock = {
|
|
149
|
+
readAll,
|
|
150
|
+
writeFiles,
|
|
151
|
+
clear: clearAll,
|
|
152
|
+
snapshot: () => askWorker({ type: 'sasjs-snapshot' }, 'sasjs-snapshot'),
|
|
153
|
+
/**
|
|
154
|
+
* The drive total in bytes, measured by the worker: it owns the stored
|
|
155
|
+
* representation, so the figure stays the one `statSync` reports rather than
|
|
156
|
+
* the length of the strings this store happens to hold - which would count
|
|
157
|
+
* the base64 expansion and the marker of a byte entry.
|
|
158
|
+
*/
|
|
159
|
+
stats: async () => {
|
|
160
|
+
const answer = await askWorker({ type: 'sasjs-stats' }, 'sasjs-stats')
|
|
161
|
+
// a worker that cannot report fails the read, as the other drive reads
|
|
162
|
+
// do: a caller destructuring { files, bytes } must not be handed NaN, and
|
|
163
|
+
// neither must a reply of an older shape that carries no stats at all
|
|
164
|
+
if (answer.error) throw new Error(answer.error)
|
|
165
|
+
if (!answer.stats) throw new Error('sasjs-stats reply carried no stats')
|
|
166
|
+
return answer.stats
|
|
167
|
+
},
|
|
168
|
+
/**
|
|
169
|
+
* Downloads the current drive as `mock-snapshot.json`. Drop that file in the
|
|
170
|
+
* build folder as `mock-snapshot.json` and the next build ships it, so every
|
|
171
|
+
* visitor starts from this state instead of from an empty drive.
|
|
172
|
+
*/
|
|
173
|
+
download: async () => {
|
|
174
|
+
const snapshot = await askWorker(
|
|
175
|
+
{ type: 'sasjs-snapshot' },
|
|
176
|
+
'sasjs-snapshot'
|
|
177
|
+
)
|
|
178
|
+
const files = (snapshot && snapshot.files) || {}
|
|
179
|
+
const blob = new Blob([JSON.stringify({ files }, null, 2)], {
|
|
180
|
+
type: 'application/json'
|
|
181
|
+
})
|
|
182
|
+
const url = URL.createObjectURL(blob)
|
|
183
|
+
const link = document.createElement('a')
|
|
184
|
+
link.href = url
|
|
185
|
+
link.download = 'mock-snapshot.json'
|
|
186
|
+
link.click()
|
|
187
|
+
setTimeout(() => URL.revokeObjectURL(url), 10000)
|
|
188
|
+
return Object.keys(files).length
|
|
189
|
+
},
|
|
190
|
+
reset: async () => {
|
|
191
|
+
await askWorker({ type: 'sasjs-reset' }, 'sasjs-reset-ack')
|
|
192
|
+
await clearAll()
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
window.__sasjsMockReady = (async () => {
|
|
198
|
+
await navigator.serviceWorker.ready
|
|
199
|
+
const controller = await waitForController()
|
|
200
|
+
if (!controller) return { error: 'no service worker controller' }
|
|
201
|
+
|
|
202
|
+
let files = {}
|
|
203
|
+
try {
|
|
204
|
+
files = await readAll()
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error('[sasjs-mock] could not read storage: ' + e)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (Object.keys(files).length) {
|
|
210
|
+
window.__sasjsMockBoot = await askWorker(
|
|
211
|
+
{ type: 'sasjs-load', files },
|
|
212
|
+
'sasjs-load-ack'
|
|
213
|
+
)
|
|
214
|
+
} else {
|
|
215
|
+
window.__sasjsMockBoot = await askWorker(
|
|
216
|
+
{ type: 'sasjs-seed' },
|
|
217
|
+
'sasjs-seed-ack'
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
return window.__sasjsMockBoot
|
|
221
|
+
})()
|
|
222
|
+
})()
|