@notifiles/sdk 1.3.0 → 1.5.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/package.json +1 -1
- package/src/lib/async/runWithConcurrency.js +32 -0
- package/src/lib/fs/fileLock.js +11 -0
- package/src/lib/fs/importJSONAsync.js +5 -1
- package/src/template/lib/activity/updateToFile.js +4 -1
- package/src/template/sync/lib/pushNotification.js +3 -0
- package/src/template/sync/lib/textMessage.js +3 -0
- package/src/template/update/path.js +10 -6
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Runs `worker` over `items` with at most `concurrency` in flight at once.
|
|
2
|
+
// Every item runs to completion regardless of sibling failures; if any
|
|
3
|
+
// worker throws, a single aggregate error (with an `.errors` array) is
|
|
4
|
+
// thrown after all items have settled, so a failure never abandons work
|
|
5
|
+
// that was already in progress.
|
|
6
|
+
export default async function runWithConcurrency(items, concurrency, worker) {
|
|
7
|
+
const queue = [...items]
|
|
8
|
+
const errors = []
|
|
9
|
+
|
|
10
|
+
const lanes = Array.from(
|
|
11
|
+
{ length: Math.max(1, Math.min(concurrency, queue.length)) },
|
|
12
|
+
async () => {
|
|
13
|
+
while (queue.length) {
|
|
14
|
+
const item = queue.shift()
|
|
15
|
+
try {
|
|
16
|
+
await worker(item)
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error(e)
|
|
19
|
+
errors.push(e)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
await Promise.all(lanes)
|
|
26
|
+
|
|
27
|
+
if (errors.length) {
|
|
28
|
+
const error = new Error(`[notifiles] ${errors.length} of ${items.length} item(s) failed`)
|
|
29
|
+
error.errors = errors
|
|
30
|
+
throw error
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Serializes concurrent reads/writes to the same file path so a read can
|
|
2
|
+
// never observe a write mid-truncate (e.g. multiple locale versions of a
|
|
3
|
+
// template racing on the same .activity.json during a parallel sync).
|
|
4
|
+
const queues = new Map()
|
|
5
|
+
|
|
6
|
+
export default function withFileLock(key, fn) {
|
|
7
|
+
const previous = queues.get(key) || Promise.resolve()
|
|
8
|
+
const run = previous.catch(() => {}).then(fn)
|
|
9
|
+
queues.set(key, run.catch(() => {}))
|
|
10
|
+
return run
|
|
11
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "fs"
|
|
2
|
+
import { fileURLToPath } from "url"
|
|
2
3
|
import checkFileExists from "./checkFileExists.js"
|
|
4
|
+
import withFileLock from "./fileLock.js"
|
|
3
5
|
|
|
4
6
|
const expandEnvPlaceholders = (value) => {
|
|
5
7
|
if (typeof value === 'string') {
|
|
@@ -29,7 +31,9 @@ export default async _url => {
|
|
|
29
31
|
return null
|
|
30
32
|
}
|
|
31
33
|
const fileUrl = new URL(_url, import.meta.url)
|
|
32
|
-
const parsedPackageJSON =
|
|
34
|
+
const parsedPackageJSON = await withFileLock(fileURLToPath(fileUrl), async () => {
|
|
35
|
+
return JSON.parse(await fs.promises.readFile(fileUrl, 'utf8'))
|
|
36
|
+
})
|
|
33
37
|
return expandEnvPlaceholders(parsedPackageJSON)
|
|
34
38
|
// } catch (e) {
|
|
35
39
|
// console.log("[notifiles]", `importJSONAsync → `, e)
|
|
@@ -2,6 +2,7 @@ import fsPath from 'path'
|
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import checkFileExists from '../../../lib/fs/checkFileExists.js'
|
|
4
4
|
import importJSONAsync from '../../../lib/fs/importJSONAsync.js'
|
|
5
|
+
import withFileLock from '../../../lib/fs/fileLock.js'
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
export default async ({
|
|
@@ -24,7 +25,9 @@ export default async ({
|
|
|
24
25
|
...content,
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
await
|
|
28
|
+
await withFileLock(fsPath.resolve(filePath), () =>
|
|
29
|
+
fs.promises.writeFile(filePath, JSON.stringify(_content, null, 2))
|
|
30
|
+
)
|
|
28
31
|
return true
|
|
29
32
|
} catch (e) {
|
|
30
33
|
console.error(e)
|
|
@@ -9,6 +9,9 @@ export default async (props) => {
|
|
|
9
9
|
|
|
10
10
|
const { entry, settings } = props
|
|
11
11
|
const { path, pushNotification, buildPaths, } = entry
|
|
12
|
+
// pushNotification is null unless the template folder has a pushnotification.txt (set in lib/entries/getAtPath.js).
|
|
13
|
+
// Every template must ship a pushnotification.txt (an empty file is fine, see templates/social/subscription/*)
|
|
14
|
+
// or this destructure throws before the existence check below ever runs.
|
|
12
15
|
const { text, } = pushNotification
|
|
13
16
|
|
|
14
17
|
const textPath = `${path}/pushnotification.txt`
|
|
@@ -9,6 +9,9 @@ export default async (props) => {
|
|
|
9
9
|
|
|
10
10
|
const { entry, settings } = props
|
|
11
11
|
const { path, textMessage, buildPaths, } = entry
|
|
12
|
+
// textMessage is null unless the template folder has a textmessage.txt (set in lib/entries/getAtPath.js).
|
|
13
|
+
// Every template must ship a textmessage.txt (an empty file is fine, see templates/social/subscription/*)
|
|
14
|
+
// or this destructure throws before the existence check below ever runs.
|
|
12
15
|
const { text, } = textMessage
|
|
13
16
|
|
|
14
17
|
const textPath = `${path}/textmessage.txt`
|
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
getCachedTemplateHash,
|
|
13
13
|
updateTemplateHashCache
|
|
14
14
|
} from './lib/cache.js'
|
|
15
|
+
import runWithConcurrency from '../../lib/async/runWithConcurrency.js'
|
|
16
|
+
|
|
17
|
+
const DEFAULT_CONCURRENCY = 4
|
|
15
18
|
|
|
16
19
|
export default async ({
|
|
17
20
|
path,
|
|
@@ -28,13 +31,14 @@ export default async ({
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
const forceUpdate = force || settings.forceUpdate
|
|
34
|
+
const concurrency = settings.concurrency || DEFAULT_CONCURRENCY
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
let entry =
|
|
36
|
+
await runWithConcurrency(entries, concurrency, async (initialEntry) => {
|
|
37
|
+
let entry = initialEntry
|
|
34
38
|
|
|
35
39
|
switch (entry.manifest.status) {
|
|
36
40
|
case 'pause': {
|
|
37
|
-
|
|
41
|
+
return
|
|
38
42
|
}
|
|
39
43
|
default: break
|
|
40
44
|
}
|
|
@@ -43,7 +47,7 @@ export default async ({
|
|
|
43
47
|
const currentHash = await computeTemplateHash({ path: entry.path })
|
|
44
48
|
if (!forceUpdate && cachedHash && currentHash && cachedHash === currentHash) {
|
|
45
49
|
console.log(`[notifiles] Skip ${entry.slug || entry.name || entry.path}: no changes detected`)
|
|
46
|
-
|
|
50
|
+
return
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
entry = (await format({ entry, settings })).entry
|
|
@@ -53,7 +57,7 @@ export default async ({
|
|
|
53
57
|
entry = (await cloudify({ entry, settings })).entry
|
|
54
58
|
entry = (await attach({ entry, settings })).entry
|
|
55
59
|
if (settings.dryRun) {
|
|
56
|
-
|
|
60
|
+
return
|
|
57
61
|
}
|
|
58
62
|
entry = (await sync({ entry, settings })).entry
|
|
59
63
|
|
|
@@ -61,7 +65,7 @@ export default async ({
|
|
|
61
65
|
if (newHash) {
|
|
62
66
|
await updateTemplateHashCache({ path: entry.path, hash: newHash })
|
|
63
67
|
}
|
|
64
|
-
}
|
|
68
|
+
})
|
|
65
69
|
|
|
66
70
|
return true
|
|
67
71
|
}
|