@summerscar/dsh-mnemon-sync 0.1.2 → 0.1.4
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/cordis.patch.yml +8 -0
- package/index.js +2 -102
- package/package.json +3 -2
- package/source.js +98 -37
- package/targets.js +24 -2
package/cordis.patch.yml
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# Host entry: HTTP API, scheduler and backup engine. Needs only webServer.
|
|
1
2
|
- insert:
|
|
2
3
|
- id: mnemon-sync
|
|
3
4
|
name: '@summerscar/dsh-mnemon-sync'
|
|
5
|
+
# Memory Source entry, mounted exactly like the shipped dsh-mnemon-source-*
|
|
6
|
+
# packages: only id + name here, and the module itself exports
|
|
7
|
+
# `inject = ['mnemonMemory']` so the Loader waits for that service.
|
|
8
|
+
# Kept as a separate Entry so a profile without dsh-mnemon still gets a
|
|
9
|
+
# working backup engine (just no tab).
|
|
10
|
+
- id: mnemon-sync-source
|
|
11
|
+
name: '@summerscar/dsh-mnemon-sync/source'
|
package/index.js
CHANGED
|
@@ -14,14 +14,13 @@
|
|
|
14
14
|
* resident process, which is both simpler and compatible with the store.
|
|
15
15
|
*
|
|
16
16
|
* The browser half only ever sees JSON: credentials are masked on the way out
|
|
17
|
-
* and are never echoed back by a read.
|
|
17
|
+
* and are never echoed back by a read. The memory Source that owns the page is
|
|
18
|
+
* a separate Cordis entry (./source.js), mounted by the bundle patch.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { createHash } from 'node:crypto'
|
|
21
22
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
22
|
-
import { createRequire } from 'node:module'
|
|
23
23
|
import { dirname, join, resolve } from 'node:path'
|
|
24
|
-
import { pathToFileURL } from 'node:url'
|
|
25
24
|
|
|
26
25
|
import { SyncHistory, SyncScheduler } from './scheduler.js'
|
|
27
26
|
import {
|
|
@@ -165,7 +164,6 @@ export function apply(ctx, config = {}) {
|
|
|
165
164
|
// — not UI contributions. Standing up this minimal Source is what lets the
|
|
166
165
|
// page use its own type instead of borrowing `memory-spaces`, which is the
|
|
167
166
|
// only way its tab can highlight independently.
|
|
168
|
-
ctx.effect(() => registerSyncSource(ctx, dataRoot), 'mnemon-sync: memory source')
|
|
169
167
|
|
|
170
168
|
ctx.effect(() => ctx.webServer.register({
|
|
171
169
|
kind: 'prefix',
|
|
@@ -582,104 +580,6 @@ function restoreMaskedSecrets(incoming, previous) {
|
|
|
582
580
|
return incoming
|
|
583
581
|
}
|
|
584
582
|
|
|
585
|
-
/**
|
|
586
|
-
* Resolve the dsh-mnemon extension SDK.
|
|
587
|
-
*
|
|
588
|
-
* A bare `import('dsh-mnemon/extension-sdk')` is not enough here. Installed
|
|
589
|
-
* from npm the package sits at `<profile>/node_modules/@summerscar/...`, so
|
|
590
|
-
* Node's own ancestor walk would find dsh-mnemon beside it — but a *linked*
|
|
591
|
-
* checkout lives outside the profile with no `node_modules` of its own, and
|
|
592
|
-
* then the walk finds nothing.
|
|
593
|
-
*
|
|
594
|
-
* Both install shapes are therefore covered, cheapest first:
|
|
595
|
-
* 1. From this module's own location, which is right for an npm install.
|
|
596
|
-
* 2. From `DSH_PROFILE_DIR`, which is right for a linked checkout and is set
|
|
597
|
-
* in every shell and Host process a profile launches.
|
|
598
|
-
* 3. From `DSH_HOME/profiles/web`, for a Host booted without that variable.
|
|
599
|
-
*
|
|
600
|
-
* Returns `undefined` when dsh-mnemon is not installed at all, which is a
|
|
601
|
-
* supported configuration: the backup keeps working without its page.
|
|
602
|
-
*/
|
|
603
|
-
async function loadExtensionSdk() {
|
|
604
|
-
// 1. Ask Node, anchored at this file. Resolves for an npm install.
|
|
605
|
-
try {
|
|
606
|
-
const resolver = createRequire(import.meta.url)
|
|
607
|
-
const resolved = resolver.resolve('dsh-mnemon/lib/extension-sdk.js')
|
|
608
|
-
return await import(pathToFileURL(resolved).href)
|
|
609
|
-
} catch {
|
|
610
|
-
// Not resolvable from here: fall through to the profile-anchored paths.
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const candidates = []
|
|
614
|
-
const profileDir = process.env.DSH_PROFILE_DIR?.trim()
|
|
615
|
-
if (profileDir !== undefined && profileDir !== '') {
|
|
616
|
-
candidates.push(join(profileDir, 'node_modules', 'dsh-mnemon', 'lib', 'extension-sdk.js'))
|
|
617
|
-
}
|
|
618
|
-
// The Host that loads this plugin lives in the installation's own tree.
|
|
619
|
-
candidates.push(join(process.env.DSH_HOME ?? '', 'profiles', 'web', 'node_modules', 'dsh-mnemon', 'lib', 'extension-sdk.js'))
|
|
620
|
-
for (const candidate of candidates) {
|
|
621
|
-
if (candidate === '' || !existsSync(candidate)) continue
|
|
622
|
-
return import(pathToFileURL(candidate).href)
|
|
623
|
-
}
|
|
624
|
-
return undefined
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Install the sync Source into dsh-mnemon's composable-memory service.
|
|
629
|
-
*
|
|
630
|
-
* Returns a disposer, and does nothing at all when the Host runs without that
|
|
631
|
-
* service (a profile with dsh-mnemon absent).
|
|
632
|
-
*/
|
|
633
|
-
function registerSyncSource(ctx, dataRoot) {
|
|
634
|
-
if (typeof ctx.get !== 'function') return () => {}
|
|
635
|
-
const service = ctx.get('mnemonMemory')
|
|
636
|
-
if (service === undefined || service === null || typeof service.installContributions !== 'function') {
|
|
637
|
-
return () => {}
|
|
638
|
-
}
|
|
639
|
-
let disposed = false
|
|
640
|
-
let uninstall
|
|
641
|
-
void (async () => {
|
|
642
|
-
try {
|
|
643
|
-
const sdk = await loadExtensionSdk()
|
|
644
|
-
if (sdk === undefined || typeof sdk.defineMemorySource !== 'function') {
|
|
645
|
-
console.warn('mnemon-sync: dsh-mnemon extension SDK not found; the sync tab stays hidden')
|
|
646
|
-
return
|
|
647
|
-
}
|
|
648
|
-
const { createSyncSource, syncSourceManifest } = await import('./source.js')
|
|
649
|
-
if (disposed) return
|
|
650
|
-
// The manifest goes through the SDK validator so a contract drift in
|
|
651
|
-
// dsh-mnemon fails loudly here instead of at composition time.
|
|
652
|
-
const definition = sdk.defineMemorySource(createSyncSource({ ...syncSourceManifest }))
|
|
653
|
-
uninstall = service.installContributions({
|
|
654
|
-
plugin: {
|
|
655
|
-
apiVersion: 'dsh-mnemon/plugin/v1',
|
|
656
|
-
packageName: '@summerscar/dsh-mnemon-sync',
|
|
657
|
-
label: { en: 'Sync backup', 'zh-CN': '同步备份' },
|
|
658
|
-
description: {
|
|
659
|
-
en: 'Archive memory spaces to S3 or WebDAV.',
|
|
660
|
-
'zh-CN': '把记忆空间备份到 S3 或 WebDAV。',
|
|
661
|
-
},
|
|
662
|
-
roles: ['source'],
|
|
663
|
-
provides: [{ id: 'source' }],
|
|
664
|
-
},
|
|
665
|
-
sources: [definition],
|
|
666
|
-
}, { instanceId: 'mnemon-sync' })
|
|
667
|
-
if (disposed) {
|
|
668
|
-
uninstall?.()
|
|
669
|
-
uninstall = undefined
|
|
670
|
-
}
|
|
671
|
-
} catch (error) {
|
|
672
|
-
// A Host that cannot accept the Source must not take the backup down.
|
|
673
|
-
console.warn('mnemon-sync: could not register the sync source', error)
|
|
674
|
-
}
|
|
675
|
-
})()
|
|
676
|
-
return () => {
|
|
677
|
-
disposed = true
|
|
678
|
-
uninstall?.()
|
|
679
|
-
uninstall = undefined
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
583
|
/** Read a staging tree into the flat `{ path, bytes }` entries the ZIP writer takes. */
|
|
684
584
|
function collect(root, prefix = '') {
|
|
685
585
|
const entries = []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@summerscar/dsh-mnemon-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Back up and restore dsh-mnemon memory components (memory spaces, project documents, runtime memory) to S3-compatible storage or WebDAV.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
".": "./index.js",
|
|
9
9
|
"./client": "./client.js",
|
|
10
10
|
"./locale/*.json": "./locale/*.json",
|
|
11
|
-
"./icon": "./icon.svg"
|
|
11
|
+
"./icon": "./icon.svg",
|
|
12
|
+
"./source": "./source.js"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"index.js",
|
package/source.js
CHANGED
|
@@ -1,34 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mnemon Sync — the Source that owns this page.
|
|
3
3
|
*
|
|
4
|
+
* This module is a standard Cordis plugin entry, written the same way the
|
|
5
|
+
* shipped dsh-mnemon Source packages are (see dsh-mnemon-source-runtime):
|
|
6
|
+
* `inject = ['mnemonMemory']` and `installMemory(ctx, ...)` inside `apply`.
|
|
7
|
+
* The Loader waits for the service, calls apply when it appears, and disposes
|
|
8
|
+
* the registration when it goes away.
|
|
9
|
+
*
|
|
10
|
+
* One concession to being distributable: the SDK is loaded with `createRequire`
|
|
11
|
+
* instead of a static `import`. A static import resolves fine for an npm
|
|
12
|
+
* install (the package sits under `<profile>/node_modules/`, with dsh-mnemon
|
|
13
|
+
* as a sibling ancestor) but fails for a `link:` checkout that lives outside
|
|
14
|
+
* the profile and has no `node_modules` of its own. `createRequire` with an
|
|
15
|
+
* explicit `DSH_PROFILE_DIR` fallback covers both shapes.
|
|
16
|
+
*
|
|
4
17
|
* Why a Source is required at all: dsh-mnemon renders a page tab only when its
|
|
5
|
-
* `sourceTypeId` appears in the live source catalog, and
|
|
18
|
+
* `sourceTypeId` appears in the live source catalog, and that catalog lists
|
|
6
19
|
* real, running Source implementations. A UI-only contribution can borrow
|
|
7
20
|
* `memory-spaces` and be shown, but the tab bar highlights *every* tab sharing
|
|
8
21
|
* the selected page's type prefix, so borrowing means clicking one lights up
|
|
9
22
|
* the other. Declaring our own type is the only way to get an independent tab.
|
|
10
23
|
*
|
|
11
24
|
* This Source is deliberately honest about what it holds. It is not a memory
|
|
12
|
-
* backend: it stores no insights,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* report only what the backup configuration currently is.
|
|
16
|
-
*
|
|
17
|
-
* The alternative — faking routes that answer with nothing — would pollute the
|
|
18
|
-
* model's memory view with an empty source and could make a composition look
|
|
19
|
-
* incomplete. An empty, clearly-labelled source is the smaller lie.
|
|
25
|
+
* backend: it stores no insights, contributes an empty projection and declares
|
|
26
|
+
* no read routes or write actions. Its role is to be the identity the sync
|
|
27
|
+
* page hangs from.
|
|
20
28
|
*/
|
|
21
29
|
|
|
22
30
|
import { createHash } from 'node:crypto'
|
|
31
|
+
import { existsSync } from 'node:fs'
|
|
32
|
+
import { createRequire } from 'node:module'
|
|
23
33
|
import { join } from 'node:path'
|
|
24
34
|
|
|
25
35
|
import { resolveDataRoot } from './index.js'
|
|
26
36
|
import { inspectHome } from './snapshot.js'
|
|
27
37
|
import { readSettings } from './store.js'
|
|
28
38
|
|
|
39
|
+
const require = createRequire(import.meta.url)
|
|
40
|
+
|
|
29
41
|
export const SYNC_SOURCE_TYPE_ID = 'mnemon-sync'
|
|
30
42
|
export const SYNC_PACKAGE_NAME = '@summerscar/dsh-mnemon-sync'
|
|
31
43
|
|
|
44
|
+
export const name = SYNC_PACKAGE_NAME
|
|
45
|
+
// The one dependency the Source needs. The Loader holds this Entry until
|
|
46
|
+
// dsh-mnemon publishes mnemonMemory, then mounts it.
|
|
47
|
+
export const inject = ['mnemonMemory']
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the extension SDK synchronously so `apply` can stay synchronous.
|
|
51
|
+
* Tries normal package resolution first, then the profile-anchored absolute
|
|
52
|
+
* path for linked checkouts.
|
|
53
|
+
*/
|
|
54
|
+
function loadSdk() {
|
|
55
|
+
try {
|
|
56
|
+
return require('dsh-mnemon/extension-sdk')
|
|
57
|
+
} catch {
|
|
58
|
+
// Fall through: linked checkout has no ancestor node_modules.
|
|
59
|
+
}
|
|
60
|
+
const profileDir = process.env.DSH_PROFILE_DIR?.trim()
|
|
61
|
+
const candidates = []
|
|
62
|
+
if (profileDir !== undefined && profileDir !== '') {
|
|
63
|
+
candidates.push(join(profileDir, 'node_modules', 'dsh-mnemon', 'lib', 'extension-sdk.js'))
|
|
64
|
+
}
|
|
65
|
+
candidates.push(join(process.env.DSH_HOME ?? '', 'profiles', 'web', 'node_modules', 'dsh-mnemon', 'lib', 'extension-sdk.js'))
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
if (candidate === '' || !existsSync(candidate)) continue
|
|
68
|
+
return require(candidate)
|
|
69
|
+
}
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sdk = loadSdk()
|
|
74
|
+
|
|
75
|
+
/** The plugin descriptor, validated by the SDK when available. */
|
|
76
|
+
export const memoryPlugin = sdk === undefined ? {
|
|
77
|
+
packageName: SYNC_PACKAGE_NAME,
|
|
78
|
+
label: { en: 'Sync backup', 'zh-CN': '同步备份' },
|
|
79
|
+
description: {
|
|
80
|
+
en: 'Back up and restore dsh-mnemon memory components.',
|
|
81
|
+
'zh-CN': '备份与恢复 dsh-mnemon 的记忆组件。',
|
|
82
|
+
},
|
|
83
|
+
roles: ['source'],
|
|
84
|
+
provides: [{ id: 'source' }],
|
|
85
|
+
} : sdk.defineMemoryPlugin({
|
|
86
|
+
packageName: SYNC_PACKAGE_NAME,
|
|
87
|
+
label: { en: 'Sync backup', 'zh-CN': '同步备份' },
|
|
88
|
+
description: {
|
|
89
|
+
en: 'Back up and restore dsh-mnemon memory components.',
|
|
90
|
+
'zh-CN': '备份与恢复 dsh-mnemon 的记忆组件。',
|
|
91
|
+
},
|
|
92
|
+
roles: ['source'],
|
|
93
|
+
provides: [{ id: 'source' }],
|
|
94
|
+
})
|
|
95
|
+
|
|
32
96
|
/** The manifest Core validates before this Source may take part in a View. */
|
|
33
97
|
export const syncSourceManifest = {
|
|
34
98
|
apiVersion: 'dsh-mnemon/v1',
|
|
@@ -43,30 +107,21 @@ export const syncSourceManifest = {
|
|
|
43
107
|
consistency: 'exact-snapshot',
|
|
44
108
|
routes: [],
|
|
45
109
|
actions: [],
|
|
46
|
-
// A management descriptor is
|
|
47
|
-
//
|
|
48
|
-
// a layer is "running" by whether its type is in that catalog — so a Source
|
|
49
|
-
// without `management` is installed but reported as "on yet not running".
|
|
50
|
-
// Fields stay empty: this page does its own editing.
|
|
110
|
+
// A management descriptor is required for managementCatalog to list this
|
|
111
|
+
// Source; without it the Source is installed but reported "on yet not running".
|
|
51
112
|
management: {
|
|
52
113
|
label: 'Sync backup',
|
|
53
|
-
description: '
|
|
114
|
+
description: 'Back up and restore dsh-mnemon memory components.',
|
|
54
115
|
},
|
|
55
116
|
}
|
|
56
117
|
|
|
57
118
|
/**
|
|
58
119
|
* Build the Source definition.
|
|
59
120
|
*
|
|
60
|
-
* `facts()`
|
|
121
|
+
* `facts()` returns the Host's facts envelope — `sourceInstanceKey`,
|
|
61
122
|
* `sourceTypeId`, `role`, `availability`, `revision`, `capabilities`,
|
|
62
|
-
* `routeIds`, `actionIds` (plus optional `hints`).
|
|
63
|
-
*
|
|
64
|
-
* registry key and throws `memory Source facts instance mismatch` when it is
|
|
65
|
-
* missing, which is exactly what a `{ summary, details }` shape produced.
|
|
66
|
-
*
|
|
67
|
-
* `hints` is the only free-form channel that survives normalization, so the
|
|
68
|
-
* backup configuration a human may want to see travels there. `project()`
|
|
69
|
-
* returns `{ fragments: [] }`: there is no memory content here to contribute.
|
|
123
|
+
* `routeIds`, `actionIds` (plus optional `hints`). `project()` brings nothing
|
|
124
|
+
* into a prompt, because there is no memory content here to bring.
|
|
70
125
|
*/
|
|
71
126
|
export function createSyncSource(manifest = syncSourceManifest) {
|
|
72
127
|
return {
|
|
@@ -81,21 +136,18 @@ export function createSyncSource(manifest = syncSourceManifest) {
|
|
|
81
136
|
return {
|
|
82
137
|
facts() {
|
|
83
138
|
const settings = safeSettings(home)
|
|
84
|
-
const inspected = safeInspect(
|
|
139
|
+
const inspected = safeInspect(home)
|
|
85
140
|
return {
|
|
86
141
|
sourceInstanceKey: context.sourceInstanceKey,
|
|
87
142
|
sourceTypeId: manifest.typeId,
|
|
88
143
|
role: manifest.role,
|
|
89
|
-
// This Source is a maintenance identity, not a memory backend: it
|
|
90
|
-
// is always ready and holds nothing a View could select.
|
|
91
144
|
availability: 'ready',
|
|
92
145
|
revision: configurationRevision(settings, inspected),
|
|
93
146
|
capabilities: [],
|
|
94
147
|
routeIds: [],
|
|
95
148
|
actionIds: [],
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// normalization, so anything worth showing goes here.
|
|
149
|
+
// Only `hints` survives normalization, so the backup configuration
|
|
150
|
+
// a human may want to see travels there.
|
|
99
151
|
hints: {
|
|
100
152
|
summary: settings.enabled
|
|
101
153
|
? `Sync backup every ${String(settings.autoSyncMinutes)} min to ${describeTarget(settings.target)}`
|
|
@@ -114,7 +166,6 @@ export function createSyncSource(manifest = syncSourceManifest) {
|
|
|
114
166
|
}
|
|
115
167
|
},
|
|
116
168
|
project() {
|
|
117
|
-
// Nothing to inject: this Source holds no memories to contribute.
|
|
118
169
|
return { fragments: [] }
|
|
119
170
|
},
|
|
120
171
|
dispose() {},
|
|
@@ -123,13 +174,23 @@ export function createSyncSource(manifest = syncSourceManifest) {
|
|
|
123
174
|
}
|
|
124
175
|
}
|
|
125
176
|
|
|
177
|
+
/** The standard Cordis entry point, identical in shape to the shipped Sources. */
|
|
178
|
+
export function apply(ctx, config = {}) {
|
|
179
|
+
if (sdk === undefined) {
|
|
180
|
+
// dsh-mnemon is not installed at all; the backup engine in index.js still
|
|
181
|
+
// works, only the page is unavailable. Do not fake a registration.
|
|
182
|
+
console.warn('mnemon-sync: dsh-mnemon extension SDK not found; the sync tab stays hidden')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
sdk.installMemory(ctx, {
|
|
186
|
+
plugin: memoryPlugin,
|
|
187
|
+
sources: [sdk.defineMemorySource(createSyncSource({ ...syncSourceManifest }))],
|
|
188
|
+
}, config?.instanceId === undefined ? undefined : { instanceId: config.instanceId })
|
|
189
|
+
}
|
|
190
|
+
|
|
126
191
|
/**
|
|
127
|
-
* A stable revision for the current backup configuration
|
|
128
|
-
*
|
|
129
|
-
* The Host requires a non-empty string and compares it across a turn, so this
|
|
130
|
-
* must change when the configuration changes and stay equal otherwise. It is
|
|
131
|
-
* derived from the facts themselves rather than a clock, which keeps
|
|
132
|
-
* composition deterministic.
|
|
192
|
+
* A stable revision for the current backup configuration, derived from the
|
|
193
|
+
* facts themselves rather than a clock so composition stays deterministic.
|
|
133
194
|
*/
|
|
134
195
|
function configurationRevision(settings, inspected) {
|
|
135
196
|
const material = JSON.stringify({
|
package/targets.js
CHANGED
|
@@ -266,7 +266,29 @@ function parseWebDavListing(xml, remotePath) {
|
|
|
266
266
|
modifiedAt: textOf(/<[^:>]*:?getlastmodified[^>]*>([^<]+)</iu.exec(block)),
|
|
267
267
|
})
|
|
268
268
|
}
|
|
269
|
-
return entries
|
|
269
|
+
return newestFirst(entries)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Sort archives newest first.
|
|
274
|
+
*
|
|
275
|
+
* `modifiedAt` is an RFC 1123 string (`Sun, 09 Nov 2026 08:00:00 GMT`) from
|
|
276
|
+
* WebDAV and an ISO 8601 string from S3. Comparing those as text is wrong: the
|
|
277
|
+
* weekday and day lead the WebDAV form, so "09 Nov 23:00" sorts before
|
|
278
|
+
* "10 Nov 01:00" even though it is older. Parse to an instant instead.
|
|
279
|
+
*
|
|
280
|
+
* An unparseable or missing stamp sorts last rather than corrupting the order:
|
|
281
|
+
* a listing that cannot say when something was written should not push a
|
|
282
|
+
* usable row off the top.
|
|
283
|
+
*/
|
|
284
|
+
function newestFirst(entries) {
|
|
285
|
+
const stamp = (entry) => {
|
|
286
|
+
const value = entry?.modifiedAt
|
|
287
|
+
if (typeof value !== 'string') return Number.NEGATIVE_INFINITY
|
|
288
|
+
const parsed = Date.parse(value)
|
|
289
|
+
return Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed
|
|
290
|
+
}
|
|
291
|
+
return entries.sort((left, right) => stamp(right) - stamp(left))
|
|
270
292
|
}
|
|
271
293
|
|
|
272
294
|
function integerOf(match) {
|
|
@@ -465,7 +487,7 @@ async function listS3(target, signal) {
|
|
|
465
487
|
modifiedAt: textOf(/<LastModified>([^<]+)<\/LastModified>/u.exec(block)),
|
|
466
488
|
})
|
|
467
489
|
}
|
|
468
|
-
return entries
|
|
490
|
+
return newestFirst(entries)
|
|
469
491
|
}
|
|
470
492
|
|
|
471
493
|
/**
|