dsh-file-activity 0.5.0 → 0.5.2
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/CHANGELOG.md +19 -0
- package/README.md +3 -4
- package/lib/api-route.js +18 -4
- package/lib/client.js +1270 -845
- package/lib/cwd.js +14 -0
- package/lib/index.js +20 -12
- package/lib/media-route.js +8 -3
- package/lib/parts/api.part.js +41 -41
- package/lib/parts/apply.part.js +69 -60
- package/lib/parts/auto-open.part.js +69 -68
- package/lib/parts/format.part.js +26 -26
- package/lib/parts/i18n.part.js +41 -40
- package/lib/parts/interceptor.part.js +50 -50
- package/lib/parts/preview.part.js +205 -160
- package/lib/parts/rows.part.js +157 -113
- package/lib/parts/store.part.js +16 -16
- package/lib/parts/styles.part.js +8 -5
- package/lib/parts/tree.part.js +79 -66
- package/lib/parts/view.part.js +172 -118
- package/lib/state.js +15 -8
- package/lib/store.js +4 -11
- package/package.json +3 -2
- package/lib/fence.js +0 -57
- package/lib/http.js +0 -34
- package/lib/parts/icons.part.js +0 -48
package/lib/parts/rows.part.js
CHANGED
|
@@ -1,126 +1,170 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// ── row rendering helpers (recent list & stats tree) ──────────────────
|
|
2
|
+
const opClass = (op) =>
|
|
3
|
+
op === 'create'
|
|
4
|
+
? 'dfa-op-create'
|
|
5
|
+
: op === 'modify'
|
|
6
|
+
? 'dfa-op-modify'
|
|
7
|
+
: op === 'delete'
|
|
8
|
+
? 'dfa-op-delete'
|
|
9
|
+
: 'dfa-op-read'
|
|
10
|
+
const opLabel = (op) =>
|
|
11
|
+
op === 'create'
|
|
12
|
+
? strings.create()
|
|
13
|
+
: op === 'modify'
|
|
14
|
+
? strings.modify()
|
|
15
|
+
: op === 'delete'
|
|
16
|
+
? strings.delete()
|
|
17
|
+
: strings.read()
|
|
4
18
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
/** Tooltip for a stats file row: absolute path + created / last-seen times. */
|
|
20
|
+
const fileTitle = (abs, firstSeen, lastSeen) => {
|
|
21
|
+
const times = []
|
|
22
|
+
if (typeof firstSeen === 'number') times.push(`${strings.created()} ${formatTime(firstSeen)}`)
|
|
23
|
+
if (typeof lastSeen === 'number') times.push(`${strings.lastSeen()} ${formatTime(lastSeen)}`)
|
|
24
|
+
return times.length > 0 ? `${abs}\n${times.join(' · ')}` : abs
|
|
25
|
+
}
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
/** Count pills for a file/dir node — only actions that actually happened are
|
|
28
|
+
* shown (a zero count renders no pill; all-zero nodes render no pill group,
|
|
29
|
+
* keeping untouched files visually quiet). */
|
|
30
|
+
const countPills = (node) => {
|
|
31
|
+
const pills = []
|
|
32
|
+
if (node.read > 0)
|
|
33
|
+
pills.push(createElement('span', { className: 'dfa-count dfa-count-read' }, `${strings.readShort()} ${node.read}`))
|
|
34
|
+
if (node.create > 0)
|
|
35
|
+
pills.push(
|
|
36
|
+
createElement('span', { className: 'dfa-count dfa-count-create' }, `${strings.createShort()} ${node.create}`),
|
|
37
|
+
)
|
|
38
|
+
if (node.modify > 0)
|
|
39
|
+
pills.push(
|
|
40
|
+
createElement('span', { className: 'dfa-count dfa-count-modify' }, `${strings.modifyShort()} ${node.modify}`),
|
|
41
|
+
)
|
|
42
|
+
if (pills.length === 0) return null
|
|
43
|
+
return createElement('span', { className: 'dfa-counts', style: { paddingLeft: '6px' } }, ...pills)
|
|
44
|
+
}
|
|
24
45
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
title: fileTitle(file.abs, file.firstSeen, file.lastSeen),
|
|
35
|
-
},
|
|
36
|
-
createElement('span', { className: 'dfa-row-icon dfa-icon-file' }, icon.file(14)),
|
|
37
|
-
createElement('span', { className: 'dfa-row-name dfa-name-file' }, file.name),
|
|
38
|
-
countPills(file),
|
|
39
|
-
file.lastSeen
|
|
40
|
-
? createElement('span', { className: 'dfa-time' }, formatRelative(file.lastSeen))
|
|
41
|
-
: null,
|
|
42
|
-
)
|
|
46
|
+
/** Extension of a file name (lowercase, no leading dot); '' when none.
|
|
47
|
+
* Dotfiles map to their whole name ('.gitignore' → 'gitignore') so the
|
|
48
|
+
* badge table can cover them; 'notes.' still yields ''. */
|
|
49
|
+
const extOf = (name) => {
|
|
50
|
+
const dot = name.lastIndexOf('.')
|
|
51
|
+
if (dot > 0) return name.slice(dot + 1).toLowerCase()
|
|
52
|
+
if (dot === 0) return name.slice(1).toLowerCase()
|
|
53
|
+
return ''
|
|
54
|
+
}
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{ key: node.path },
|
|
51
|
-
createElement(
|
|
52
|
-
'div',
|
|
53
|
-
{
|
|
54
|
-
className: 'dfa-row dfa-row-dir',
|
|
55
|
-
onClick: () => onToggleDir(node.path),
|
|
56
|
-
style: { paddingLeft: 8 + depth * 20 },
|
|
57
|
-
title: `${node.path}/`,
|
|
58
|
-
},
|
|
59
|
-
createElement('span', { className: 'dfa-chevron' },
|
|
60
|
-
collapsed ? icon.chevronRight(13) : icon.chevronDown(13),
|
|
61
|
-
),
|
|
62
|
-
createElement('span', { className: 'dfa-row-icon dfa-icon-folder' }, icon.folder(14)),
|
|
63
|
-
createElement('span', { className: 'dfa-row-name' },
|
|
64
|
-
node.compressed ? node.name : node.name + '/',
|
|
65
|
-
),
|
|
66
|
-
countPills(node),
|
|
67
|
-
),
|
|
68
|
-
collapsed ? null : node.children.map((child) => renderTreeNode(child, depth + 1, collapsedDirs, onToggleDir, onOpen)),
|
|
69
|
-
)
|
|
70
|
-
}
|
|
56
|
+
/** Extension-less but common build files → their badge key. */
|
|
57
|
+
const NAME_BADGES = {
|
|
58
|
+
makefile: 'makefile',
|
|
59
|
+
dockerfile: 'dockerfile',
|
|
60
|
+
'cmakelists.txt': 'cmake',
|
|
61
|
+
}
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
63
|
+
/** Badge key for a file name: basename match first, then extension. */
|
|
64
|
+
const badgeKeyOf = (name) => {
|
|
65
|
+
const base = name.toLowerCase()
|
|
66
|
+
const named = NAME_BADGES[base]
|
|
67
|
+
if (named !== undefined) return named
|
|
68
|
+
return extOf(name)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A stats-tree file row: icon + name + count pills + relative time. */
|
|
72
|
+
const fileRow = (file, depth, onOpen) =>
|
|
73
|
+
createElement(
|
|
74
|
+
'div',
|
|
75
|
+
{
|
|
76
|
+
key: file.abs,
|
|
77
|
+
className: 'dfa-row',
|
|
78
|
+
onClick: () => onOpen(file.abs),
|
|
79
|
+
style: { paddingLeft: 8 + depth * 20 },
|
|
80
|
+
title: fileTitle(file.abs, file.firstSeen, file.lastSeen),
|
|
81
|
+
},
|
|
82
|
+
createElement('span', { className: 'dfa-row-icon dfa-icon-file' }, fileIconByExt(badgeKeyOf(file.name))),
|
|
83
|
+
createElement('span', { className: 'dfa-row-name dfa-name-file' }, file.name),
|
|
84
|
+
countPills(file),
|
|
85
|
+
file.lastSeen ? createElement('span', { className: 'dfa-time' }, formatRelative(file.lastSeen)) : null,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
/** One stats-tree node: file rows render inline, dirs toggle collapse. */
|
|
89
|
+
function renderTreeNode(node, depth, collapsedDirs, onToggleDir, onOpen) {
|
|
90
|
+
if (node.type === 'file') return fileRow(node, depth, onOpen)
|
|
91
|
+
const collapsed = collapsedDirs.has(node.path)
|
|
92
|
+
return createElement(
|
|
93
|
+
'div',
|
|
94
|
+
{ key: node.path },
|
|
95
|
+
createElement(
|
|
96
|
+
'div',
|
|
97
|
+
{
|
|
98
|
+
className: 'dfa-row dfa-row-dir',
|
|
99
|
+
onClick: () => onToggleDir(node.path),
|
|
100
|
+
style: { paddingLeft: 8 + depth * 20 },
|
|
101
|
+
title: `${node.path}/`,
|
|
102
|
+
},
|
|
103
|
+
createElement('span', { className: 'dfa-chevron' }, collapsed ? icon.chevronRight(13) : icon.chevronDown(13)),
|
|
104
|
+
createElement('span', { className: 'dfa-row-icon dfa-icon-folder' }, icon.folder(14)),
|
|
105
|
+
createElement('span', { className: 'dfa-row-name' }, node.compressed ? node.name : node.name + '/'),
|
|
106
|
+
countPills(node),
|
|
107
|
+
),
|
|
108
|
+
collapsed
|
|
109
|
+
? null
|
|
110
|
+
: node.children.map((child) => renderTreeNode(child, depth + 1, collapsedDirs, onToggleDir, onOpen)),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
86
113
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
/** A recent-list row: op badge + basename + relative time. */
|
|
115
|
+
const recentEntry = (entry, onOpen) =>
|
|
116
|
+
createElement(
|
|
117
|
+
'div',
|
|
118
|
+
{
|
|
119
|
+
key: `${entry.path}:${entry.time}:${entry.op}`,
|
|
120
|
+
className: 'dfa-row',
|
|
121
|
+
onClick: () => onOpen(entry.path),
|
|
122
|
+
title: entry.path,
|
|
123
|
+
},
|
|
124
|
+
createElement('span', { className: `dfa-op ${opClass(entry.op)}` }, opLabel(entry.op)),
|
|
125
|
+
createElement('span', { className: 'dfa-row-name' }, basenameOf(entry.path)),
|
|
126
|
+
createElement('span', { className: 'dfa-time' }, formatRelative(entry.time)),
|
|
127
|
+
)
|
|
94
128
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
/** Toggle a key in a Set (directory collapse state). */
|
|
130
|
+
function toggleInSet(set, key) {
|
|
131
|
+
const next = new Set(set)
|
|
132
|
+
if (next.has(key)) next.delete(key)
|
|
133
|
+
else next.add(key)
|
|
134
|
+
return next
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Clear the current session's records host-side and reset its bucket. */
|
|
138
|
+
function clearSessionData(dataStore, sessionId) {
|
|
139
|
+
if (!window.confirm(strings.clearConfirm())) return
|
|
140
|
+
postClear(sessionId)
|
|
141
|
+
const current = dataStore.getSnapshot()
|
|
142
|
+
dataStore.set({
|
|
143
|
+
bySession: {
|
|
144
|
+
...(current.bySession ?? {}),
|
|
145
|
+
[sessionId]: { recent: [], counts: {}, loading: false },
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Manual refresh: fetch stats + the authoritative cwd for this session. */
|
|
151
|
+
function refreshSessionData(dataStore, sessionId, setCwd, setError) {
|
|
152
|
+
if (sessionId === '') return
|
|
153
|
+
void fetchStats(sessionId)
|
|
154
|
+
.then((value) => {
|
|
155
|
+
if (value === null) return
|
|
156
|
+
setCwd((prev) => prev || value.cwd || '')
|
|
99
157
|
const current = dataStore.getSnapshot()
|
|
100
158
|
dataStore.set({
|
|
101
159
|
bySession: {
|
|
102
160
|
...(current.bySession ?? {}),
|
|
103
|
-
[sessionId]: { recent: [], counts: {}, loading: false },
|
|
161
|
+
[sessionId]: { recent: value.recent ?? [], counts: value.counts ?? {}, loading: false },
|
|
104
162
|
},
|
|
105
163
|
})
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
setCwd((prev) => prev || value.cwd || '')
|
|
114
|
-
const current = dataStore.getSnapshot()
|
|
115
|
-
dataStore.set({
|
|
116
|
-
bySession: {
|
|
117
|
-
...(current.bySession ?? {}),
|
|
118
|
-
[sessionId]: { recent: value.recent ?? [], counts: value.counts ?? {}, loading: false },
|
|
119
|
-
},
|
|
120
|
-
})
|
|
121
|
-
setError(false)
|
|
122
|
-
}).catch(() => setError(true))
|
|
123
|
-
void fetchSessionCwd(sessionId).then((cwd) => {
|
|
124
|
-
if (cwd !== '') setCwd(cwd)
|
|
125
|
-
})
|
|
126
|
-
}
|
|
164
|
+
setError(false)
|
|
165
|
+
})
|
|
166
|
+
.catch(() => setError(true))
|
|
167
|
+
void fetchSessionCwd(sessionId).then((cwd) => {
|
|
168
|
+
if (cwd !== '') setCwd(cwd)
|
|
169
|
+
})
|
|
170
|
+
}
|
package/lib/parts/store.part.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
// ── tiny external store ───────────────────────────────────────────────
|
|
2
|
+
function createStore(initial) {
|
|
3
|
+
let state = initial
|
|
4
|
+
const listeners = new Set()
|
|
5
|
+
return {
|
|
6
|
+
getSnapshot: () => state,
|
|
7
|
+
set(patch) {
|
|
8
|
+
state = { ...state, ...patch }
|
|
9
|
+
for (const listener of [...listeners]) listener()
|
|
10
|
+
},
|
|
11
|
+
subscribe(listener) {
|
|
12
|
+
listeners.add(listener)
|
|
13
|
+
return () => listeners.delete(listener)
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
}
|
package/lib/parts/styles.part.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// ── themed stylesheet (injected once per activation) ──────────────────
|
|
2
|
+
// Mirrors the better-sidebar explorer surface: tight 2px 6px 8px body,
|
|
3
|
+
// 30px rows, box-sizing border-box indentation, folder rows use the
|
|
4
|
+
// strong type face to read as directories, files stay regular.
|
|
5
|
+
const STYLES = `
|
|
6
6
|
.dfa { display:flex; flex-direction:column; height:100%; overflow-y:auto; overflow-x:hidden;
|
|
7
7
|
padding:2px 6px 8px; gap:2px; font:var(--dsw-font-s-14); color:var(--dsw-alias-label-primary); }
|
|
8
8
|
.dfa-iconbtn { display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; padding:0;
|
|
@@ -74,4 +74,7 @@
|
|
|
74
74
|
.dfa-pdf-download:hover { text-decoration:underline; }
|
|
75
75
|
.dfa-pdf-frame { flex:1; min-height:0; width:100%; border:none; border-radius:6px; background:transparent; }
|
|
76
76
|
@keyframes dfa-row-in { from { opacity:0; transform:translateY(1px); } to { opacity:1; transform:none; } }
|
|
77
|
+
/* issue #60: 移除 #25 的侧边栏页签选中态品牌蓝覆盖([class*="tab"][class*=
|
|
78
|
+
"tabActive"] 全局子串选择器误伤宿主对话/工作区 tab 选中态,出现用户不
|
|
79
|
+
想要的蓝色高亮)。页签选中态回归宿主(dsh-better-sidebar)默认样式。 */
|
|
77
80
|
`
|
package/lib/parts/tree.part.js
CHANGED
|
@@ -1,71 +1,84 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
// ── directory tree construction ───────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* Collapse chain directories: a directory whose only child is another
|
|
4
|
+
* directory merges into it (a → a.b → a.b.c …). Deep single-child paths
|
|
5
|
+
* render as one dotted label with the file(s) directly beneath.
|
|
6
|
+
* `root` itself is never collapsed (its name is '' and would drop the
|
|
7
|
+
* top-level directory).
|
|
8
|
+
*/
|
|
9
|
+
function compressChains(node, isRoot) {
|
|
10
|
+
for (const child of node.children) {
|
|
11
|
+
if (child.type === 'dir') compressChains(child, false)
|
|
12
|
+
}
|
|
13
|
+
if (isRoot) return
|
|
14
|
+
while (node.children.length === 1 && node.children[0].type === 'dir') {
|
|
15
|
+
const only = node.children[0]
|
|
16
|
+
node.name = `${node.name}.${only.name}`
|
|
17
|
+
node.children = only.children
|
|
18
|
+
node.compressed = true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Sort a directory node: directories first (alphabetically), then files
|
|
24
|
+
* (by total activity, then name); recurse into directories.
|
|
25
|
+
*/
|
|
26
|
+
function sortNode(node) {
|
|
27
|
+
node.children.sort((a, b) => {
|
|
28
|
+
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1
|
|
29
|
+
if (a.type === 'dir') return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
30
|
+
const ta = a.read + a.create + a.modify
|
|
31
|
+
const tb = b.read + b.create + b.modify
|
|
32
|
+
return tb - ta || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
33
|
+
})
|
|
34
|
+
for (const child of node.children) {
|
|
35
|
+
if (child.type === 'dir') sortNode(child)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
node
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Build a nested directory tree from per-file counts, keyed by the file's
|
|
41
|
+
* absolute path. Every directory node aggregates its subtree counters and
|
|
42
|
+
* sorts directories first (alphabetically), then files (by activity).
|
|
43
|
+
*/
|
|
44
|
+
function buildTree(counts) {
|
|
45
|
+
const root = { type: 'dir', name: '', path: '', children: [], read: 0, create: 0, modify: 0 }
|
|
46
|
+
for (const [abs, counter] of Object.entries(counts)) {
|
|
47
|
+
const parts = abs.split('/').filter((part) => part !== '')
|
|
48
|
+
if (parts.length === 0) continue
|
|
49
|
+
const name = parts[parts.length - 1]
|
|
50
|
+
let node = root
|
|
51
|
+
for (const dir of parts.slice(0, -1)) {
|
|
52
|
+
let child = node.children.find((c) => c.type === 'dir' && c.name === dir)
|
|
53
|
+
if (child === undefined) {
|
|
54
|
+
child = {
|
|
55
|
+
type: 'dir',
|
|
56
|
+
name: dir,
|
|
57
|
+
path: `${node.path}/${dir}`,
|
|
58
|
+
children: [],
|
|
59
|
+
read: 0,
|
|
60
|
+
create: 0,
|
|
61
|
+
modify: 0,
|
|
61
62
|
}
|
|
62
|
-
node.children.push(
|
|
63
|
-
type: 'file', name, abs,
|
|
64
|
-
read: counter.read, create: counter.create, modify: counter.modify,
|
|
65
|
-
firstSeen: counter.firstSeen, lastSeen: counter.lastSeen,
|
|
66
|
-
})
|
|
63
|
+
node.children.push(child)
|
|
67
64
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
node = child
|
|
66
|
+
node.read += counter.read
|
|
67
|
+
node.create += counter.create
|
|
68
|
+
node.modify += counter.modify
|
|
71
69
|
}
|
|
70
|
+
node.children.push({
|
|
71
|
+
type: 'file',
|
|
72
|
+
name,
|
|
73
|
+
abs,
|
|
74
|
+
read: counter.read,
|
|
75
|
+
create: counter.create,
|
|
76
|
+
modify: counter.modify,
|
|
77
|
+
firstSeen: counter.firstSeen,
|
|
78
|
+
lastSeen: counter.lastSeen,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
sortNode(root)
|
|
82
|
+
compressChains(root, true)
|
|
83
|
+
return root
|
|
84
|
+
}
|