scratch-l10n 6.1.102 → 6.1.103

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scratch-l10n",
3
- "version": "6.1.102",
3
+ "version": "6.1.103",
4
4
  "description": "Localization for the Scratch 3.0 components",
5
5
  "main": "./dist/l10n.js",
6
6
  "browser": "./src/index.mjs",
@@ -55,7 +55,7 @@
55
55
  "lodash.defaultsdeep": "4.6.1",
56
56
  "mkdirp": "3.0.1",
57
57
  "transifex": "1.6.6",
58
- "tsx": "4.23.1"
58
+ "tsx": "4.23.4"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@babel/cli": "7.29.7",
@@ -61,16 +61,22 @@ const DRY_RUN = !!process.env.DRY_RUN && process.env.DRY_RUN !== '0' && process.
61
61
  /**
62
62
  * Wall-clock budget for a single run, in minutes (override with `HELP_SYNC_MAX_MINUTES`). When it is
63
63
  * exceeded the sync stops starting new writes, persists the baseline for everything that finished, and
64
- * exits cleanly. This matters because the baseline is restored from and saved to a CI cache whose save
65
- * step only runs when the job is not cancelled: finishing under the job's hard `timeout-minutes` keeps
66
- * partial progress, so a first-time full sync that cannot complete in one run converges over several
67
- * daily runs instead of losing everything to a timeout.
64
+ * exits cleanly. Durability across a cancellation is already handled by the incremental baseline flush
65
+ * plus the workflow's save-on-cancel step, so this budget is the graceful path, not the safety net:
66
+ * exiting cleanly under the step's hard `timeout-minutes` lets the run print its failure summary, exit
67
+ * non-zero on genuine errors, and finish as a success rather than a cancellation -- so real problems
68
+ * surface (and alert) while the baseline is still converging instead of being masked by a timeout.
68
69
  */
69
70
  const RUN_TIME_BUDGET_MS = (() => {
70
71
  const minutes = Number(process.env.HELP_SYNC_MAX_MINUTES)
71
72
  return (Number.isFinite(minutes) && minutes > 0 ? minutes : 24) * 60_000
72
73
  })()
73
- /** When this run began, used to enforce {@link RUN_TIME_BUDGET_MS}. */
74
+ /**
75
+ * When this run began, used to enforce {@link RUN_TIME_BUDGET_MS}. Each sync phase (`pull:help:names`,
76
+ * `pull:help:articles`) runs as its own job step with its own step-level `timeout-minutes`, so this
77
+ * per-process clock starts alongside that step's timeout: the soft budget sits just under it, stopping
78
+ * new writes and exiting cleanly (reporting failures) before the step is hard-killed.
79
+ */
74
80
  const runStartedAt = Date.now()
75
81
  /** Count of changed, supported pairs left unattempted because the run hit its time budget. */
76
82
  let deferredForBudget = 0
@@ -192,6 +198,49 @@ const markSynced = (resource: string, locale: string): void => {
192
198
  }
193
199
  }
194
200
 
201
+ /** Minimum interval between incremental baseline flushes, to bound both progress loss and write churn. */
202
+ const BASELINE_FLUSH_INTERVAL_MS = 15_000
203
+ /** When the baseline was last flushed to disk, used to debounce {@link maybeFlushBaseline}. */
204
+ let lastBaselineFlushAt = 0
205
+
206
+ /**
207
+ * Merge the pairs synced so far into the baseline and persist it to disk. A no-op in a dry run (which
208
+ * must never mutate the baseline) and before change detection is initialized: a script that calls
209
+ * saveItem without initChangeDetection (the locale-debug pull) leaves baseline/currentStats empty, and
210
+ * persisting then would clobber the real baseline with an empty file, forcing the next sync to re-sync
211
+ * everything. Safe to call from concurrently-running saveItem tasks: JS is single-threaded and
212
+ * {@link saveBaseline} writes atomically, so calls cannot interleave a partial file.
213
+ */
214
+ const flushBaseline = (): void => {
215
+ if (DRY_RUN || !changeDetectionReady) {
216
+ return
217
+ }
218
+ for (const [key, timestamp] of syncedPairs) {
219
+ baseline.set(key, timestamp)
220
+ }
221
+ saveBaseline(baseline)
222
+ lastBaselineFlushAt = Date.now()
223
+ }
224
+
225
+ /**
226
+ * Persist the baseline if enough time has passed since the last flush. Called as the sync makes
227
+ * progress so a run cancelled mid-sync (for example by its phase step's timeout) still keeps what it
228
+ * finished on disk, letting the workflow's save-on-cancel step cache it and the next run continue
229
+ * instead of restarting a full sync. The graceful path still calls {@link flushBaseline} once more at
230
+ * the end via {@link finalizeSync}.
231
+ */
232
+ const maybeFlushBaseline = (): void => {
233
+ // Nothing to persist in a dry run or before change detection is initialized, so skip the debounce
234
+ // entirely rather than calling flushBaseline (a no-op there) on every batch. flushBaseline keeps the
235
+ // same guards for the finalizeSync path.
236
+ if (DRY_RUN || !changeDetectionReady) {
237
+ return
238
+ }
239
+ if (Date.now() - lastBaselineFlushAt >= BASELINE_FLUSH_INTERVAL_MS) {
240
+ flushBaseline()
241
+ }
242
+ }
243
+
195
244
  /**
196
245
  * Finish the run: persist the advanced baseline (unless this is a dry run) and print the failure
197
246
  * summary. Call once, after all saves have completed.
@@ -203,10 +252,7 @@ export const finalizeSync = (): void => {
203
252
  `${plannedWrites} Freshdesk write(s). No writes were issued and the baseline was left unchanged.`,
204
253
  )
205
254
  } else {
206
- for (const [key, timestamp] of syncedPairs) {
207
- baseline.set(key, timestamp)
208
- }
209
- saveBaseline(baseline)
255
+ flushBaseline()
210
256
  console.log(`Synced ${syncedPairs.size} (resource, locale) pair(s); baseline now holds ${baseline.size} entries.`)
211
257
  }
212
258
  if (deferredForBudget > 0) {
@@ -537,5 +583,8 @@ export const saveItem = async (item: TransifexResourceObject, languages: string[
537
583
  }
538
584
  }),
539
585
  )
586
+ // Persist progress periodically so a run cancelled mid-sync keeps what it finished (see
587
+ // maybeFlushBaseline); the interval debounce keeps this from writing on every batch.
588
+ maybeFlushBaseline()
540
589
  }
541
590
  }
@@ -8,7 +8,7 @@
8
8
  * The file location is overridable via `HELP_SYNC_BASELINE_FILE` so the persistence mechanism (a
9
9
  * committed repo file vs. a restored CI cache) can change without touching this code.
10
10
  */
11
- import { readFileSync, writeFileSync, mkdirSync } from 'fs'
11
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs'
12
12
  import { dirname } from 'path'
13
13
  import { messageOf } from './errors.mts'
14
14
 
@@ -42,7 +42,11 @@ export const loadBaseline = (path?: string): SyncBaseline => {
42
42
  }
43
43
 
44
44
  /**
45
- * Persist the baseline to disk, sorted by key for a stable, diff-friendly file.
45
+ * Persist the baseline to disk, sorted by key for a stable, diff-friendly file. The write is atomic
46
+ * (write a temp file, then rename over the target) because the sync flushes the baseline repeatedly
47
+ * as it makes progress and relies on the file surviving an abrupt kill (a job timeout mid-write). A
48
+ * partial `writeFileSync` would otherwise leave a truncated file that {@link loadBaseline} discards as
49
+ * corrupt, throwing away every pair synced so far. `renameSync` within the same directory is atomic.
46
50
  * @param baseline - the baseline to write
47
51
  * @param path - override for the baseline file location; defaults to `HELP_SYNC_BASELINE_FILE` or {@link DEFAULT_BASELINE_PATH}
48
52
  */
@@ -53,5 +57,7 @@ export const saveBaseline = (baseline: SyncBaseline, path?: string): void => {
53
57
  for (const key of [...baseline.keys()].sort()) {
54
58
  sorted[key] = baseline.get(key) ?? null
55
59
  }
56
- writeFileSync(file, JSON.stringify(sorted, null, 2) + '\n')
60
+ const tempFile = `${file}.tmp`
61
+ writeFileSync(tempFile, JSON.stringify(sorted, null, 2) + '\n')
62
+ renameSync(tempFile, file)
57
63
  }