@live-change/db-client 0.9.225 → 0.9.227

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/bin/client.js CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  import dump from '../lib/dump.js'
4
4
  import exec from '../lib/exec.js'
5
+ import filterDump from '../lib/filterDump.js'
6
+ import statsDump from '../lib/statsDump.js'
5
7
  import request from '../lib/request.js'
6
8
  import get from '../lib/get.js'
7
9
  import observe from '../lib/observe.js'
@@ -10,8 +12,8 @@ import parseList from '../lib/parseList.js'
10
12
  import yargs from 'yargs'
11
13
 
12
14
  process.on('unhandledRejection', (reason, event) => {
13
- console.log('Unhandled Rejection at: Promise',
14
- "reason", reason, "stack", reason.stack, "promise", reason.promise)
15
+ console.error('Unhandled Rejection at: Promise',
16
+ "reason", reason, "stack", reason && reason.stack, "promise", reason && reason.promise)
15
17
  })
16
18
 
17
19
  process.on('uncaughtException', function (err) {
@@ -52,6 +54,36 @@ function execOptions(yargs) {
52
54
  yargs.option('targetDb', {
53
55
  describe: 'target database name'
54
56
  })
57
+ yargs.option('progressFile', {
58
+ describe: 'progress file path (default lcdbc-exec.progress); set false to disable. line= is last successful request; after timeout use --fromLine <inFlightLine> to retry',
59
+ type: 'string',
60
+ default: 'lcdbc-exec.progress'
61
+ })
62
+ yargs.option('fromLine', {
63
+ describe: '1-based inclusive file line to start from (matches progress line=). After success continue with line+1; after timeout retry with inFlightLine',
64
+ type: 'number',
65
+ default: 0
66
+ })
67
+ yargs.option('skipIndex', {
68
+ describe: 'skip createIndex requests (restore tables/logs only)',
69
+ type: 'boolean',
70
+ default: false
71
+ })
72
+ yargs.option('onlyIndex', {
73
+ describe: 'run only createIndex requests (and sync barriers)',
74
+ type: 'boolean',
75
+ default: false
76
+ })
77
+ yargs.option('requestTimeout', {
78
+ describe: 'request timeout in ms (0 = no timeout)',
79
+ type: 'number',
80
+ default: 0
81
+ })
82
+ yargs.option('excludeTable', {
83
+ describe: 'exclude put/putOldLog data for table or log name (repeatable)',
84
+ type: 'string',
85
+ array: true
86
+ })
55
87
  }
56
88
 
57
89
  yargs(process.argv.slice(2)) // eslint-disable-line
@@ -125,8 +157,64 @@ yargs(process.argv.slice(2)) // eslint-disable-line
125
157
  })
126
158
  }, argv => {
127
159
  exec({
128
- serverUrl: argv.serverUrl, verbose: argv.verbose, file: argv.file,
129
- targetDb: argv.targetDb
160
+ serverUrl: argv.serverUrl,
161
+ verbose: argv.verbose,
162
+ file: argv.file,
163
+ targetDb: argv.targetDb,
164
+ progressFile: argv.progressFile,
165
+ fromLine: argv.fromLine,
166
+ skipIndex: argv.skipIndex,
167
+ onlyIndex: argv.onlyIndex,
168
+ requestTimeout: argv.requestTimeout,
169
+ excludeTable: argv.excludeTable
170
+ }).catch((error) => {
171
+ console.error(error && error.stack ? error.stack : error)
172
+ process.exit(1)
173
+ })
174
+ })
175
+ .command('filter [file]', 'filter dump jsonl to stdout (drop put/putOldLog for tables)', (yargs) => {
176
+ yargs.positional('file', {
177
+ describe: 'dump file (default stdin)',
178
+ default: '-'
179
+ })
180
+ yargs.option('excludeTable', {
181
+ describe: 'exclude put/putOldLog data for table or log name (repeatable)',
182
+ type: 'string',
183
+ array: true,
184
+ demandOption: true
185
+ })
186
+ }, argv => {
187
+ filterDump({
188
+ file: argv.file,
189
+ excludeTable: argv.excludeTable
190
+ }).catch((error) => {
191
+ console.error(error && error.stack ? error.stack : error)
192
+ process.exit(1)
193
+ })
194
+ })
195
+ .command('stats [file]', 'summarize dump put/putOldLog counts and sizes', (yargs) => {
196
+ yargs.positional('file', {
197
+ describe: 'dump file (default stdin)',
198
+ default: '-'
199
+ })
200
+ yargs.option('sort', {
201
+ describe: 'sort by bytes or entries (descending)',
202
+ choices: ['bytes', 'entries'],
203
+ default: 'bytes'
204
+ })
205
+ yargs.option('human', {
206
+ describe: 'human-readable sizes (KiB/MiB/GiB)',
207
+ type: 'boolean',
208
+ default: false
209
+ })
210
+ }, argv => {
211
+ statsDump({
212
+ file: argv.file,
213
+ sort: argv.sort,
214
+ human: argv.human
215
+ }).catch((error) => {
216
+ console.error(error && error.stack ? error.stack : error)
217
+ process.exit(1)
130
218
  })
131
219
  })
132
220
  .option('verbose', {
@@ -0,0 +1,187 @@
1
+ function parseDumpLine(line) {
2
+ const trimmed = typeof line === 'string' ? line.trim() : ''
3
+ if(!trimmed) return { skip: true }
4
+ try {
5
+ return { command: JSON.parse(trimmed) }
6
+ } catch(error) {
7
+ return {
8
+ error: error.message || String(error),
9
+ preview: trimmed.slice(0, 120)
10
+ }
11
+ }
12
+ }
13
+
14
+ function methodName(command) {
15
+ const method = command && command.method
16
+ if(!Array.isArray(method) || method.length === 0) return null
17
+ return method[method.length - 1]
18
+ }
19
+
20
+ function isCreateIndex(command) {
21
+ return methodName(command) === 'createIndex'
22
+ }
23
+
24
+ function isPut(command) {
25
+ return methodName(command) === 'put'
26
+ }
27
+
28
+ function isPutOldLog(command) {
29
+ return methodName(command) === 'putOldLog'
30
+ }
31
+
32
+ function putTableName(command) {
33
+ if(!isPut(command) && !isPutOldLog(command)) return null
34
+ const parameters = command && command.parameters
35
+ if(!Array.isArray(parameters) || parameters.length < 2) return null
36
+ return parameters[1]
37
+ }
38
+
39
+ function putObjectId(command) {
40
+ if(!isPut(command) && !isPutOldLog(command)) return null
41
+ const parameters = command && command.parameters
42
+ if(!Array.isArray(parameters) || parameters.length < 3) return null
43
+ const object = parameters[2]
44
+ if(!object || typeof object !== 'object') return null
45
+ return object.id ?? null
46
+ }
47
+
48
+ function shouldRunRequest(command, options = {}) {
49
+ const {
50
+ skipIndex = false,
51
+ onlyIndex = false,
52
+ excludeTables = []
53
+ } = options
54
+ const excludeSet = excludeTables instanceof Set
55
+ ? excludeTables
56
+ : new Set(Array.isArray(excludeTables) ? excludeTables : [])
57
+
58
+ if(command && command.type === 'sync') return true
59
+ if(!command || command.type !== 'request') return false
60
+
61
+ if(onlyIndex) return isCreateIndex(command)
62
+ if(skipIndex && isCreateIndex(command)) return false
63
+
64
+ if(excludeSet.size > 0 && (isPut(command) || isPutOldLog(command))) {
65
+ const tableName = putTableName(command)
66
+ if(tableName != null && excludeSet.has(tableName)) return false
67
+ }
68
+
69
+ return true
70
+ }
71
+
72
+ function statsMapKey(kind, name) {
73
+ return kind + '\0' + name
74
+ }
75
+
76
+ function accumulateDumpStats(statsMap, command, lineByteLength) {
77
+ if(!command || command.type !== 'request') return
78
+ let kind = null
79
+ if(isPut(command)) kind = 'table'
80
+ else if(isPutOldLog(command)) kind = 'log'
81
+ else return
82
+
83
+ const name = putTableName(command)
84
+ if(name == null) return
85
+
86
+ const bytes = Number(lineByteLength) || 0
87
+ const key = statsMapKey(kind, name)
88
+ const existing = statsMap.get(key)
89
+ if(existing) {
90
+ existing.entries += 1
91
+ existing.bytes += bytes
92
+ } else {
93
+ statsMap.set(key, { kind, name, entries: 1, bytes })
94
+ }
95
+ }
96
+
97
+ function dumpStatsRows(statsMap) {
98
+ return Array.from(statsMap.values())
99
+ }
100
+
101
+ function sortDumpStats(rows, sortBy = 'bytes') {
102
+ const key = sortBy === 'entries' ? 'entries' : 'bytes'
103
+ return [...rows].sort((a, b) => {
104
+ const diff = b[key] - a[key]
105
+ if(diff !== 0) return diff
106
+ if(a.kind !== b.kind) return a.kind < b.kind ? -1 : 1
107
+ return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)
108
+ })
109
+ }
110
+
111
+ function formatHumanBytes(bytes) {
112
+ const n = Number(bytes) || 0
113
+ if(n < 1024) return String(n)
114
+ const units = ['KiB', 'MiB', 'GiB', 'TiB']
115
+ let value = n
116
+ let unitIndex = -1
117
+ while(value >= 1024 && unitIndex < units.length - 1) {
118
+ value /= 1024
119
+ unitIndex++
120
+ }
121
+ const rounded = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2)
122
+ return rounded + units[unitIndex]
123
+ }
124
+
125
+ function formatDumpStatsTable(rows, options = {}) {
126
+ const { human = false } = options
127
+ const list = Array.isArray(rows) ? rows : []
128
+ let totalEntries = 0
129
+ let totalBytes = 0
130
+ for(const row of list) {
131
+ totalEntries += row.entries
132
+ totalBytes += row.bytes
133
+ }
134
+
135
+ const formatBytes = (n) => human ? formatHumanBytes(n) : String(n)
136
+ const kindWidth = Math.max(4, ...list.map((r) => String(r.kind).length), 0)
137
+ const nameWidth = Math.max(4, 5, ...list.map((r) => String(r.name).length))
138
+ const entriesWidth = Math.max(
139
+ 7,
140
+ String(totalEntries).length,
141
+ ...list.map((r) => String(r.entries).length)
142
+ )
143
+ const bytesWidth = Math.max(
144
+ 5,
145
+ formatBytes(totalBytes).length,
146
+ ...list.map((r) => formatBytes(r.bytes).length)
147
+ )
148
+
149
+ const lines = []
150
+ lines.push(
151
+ 'KIND'.padEnd(kindWidth) + ' ' +
152
+ 'NAME'.padEnd(nameWidth) + ' ' +
153
+ 'ENTRIES'.padStart(entriesWidth) + ' ' +
154
+ 'BYTES'.padStart(bytesWidth)
155
+ )
156
+ for(const row of list) {
157
+ lines.push(
158
+ String(row.kind).padEnd(kindWidth) + ' ' +
159
+ String(row.name).padEnd(nameWidth) + ' ' +
160
+ String(row.entries).padStart(entriesWidth) + ' ' +
161
+ formatBytes(row.bytes).padStart(bytesWidth)
162
+ )
163
+ }
164
+ lines.push(
165
+ ''.padEnd(kindWidth) + ' ' +
166
+ 'TOTAL'.padEnd(nameWidth) + ' ' +
167
+ String(totalEntries).padStart(entriesWidth) + ' ' +
168
+ formatBytes(totalBytes).padStart(bytesWidth)
169
+ )
170
+ return lines.join('\n')
171
+ }
172
+
173
+ export {
174
+ parseDumpLine,
175
+ methodName,
176
+ isCreateIndex,
177
+ isPut,
178
+ isPutOldLog,
179
+ putTableName,
180
+ putObjectId,
181
+ shouldRunRequest,
182
+ accumulateDumpStats,
183
+ dumpStatsRows,
184
+ sortDumpStats,
185
+ formatHumanBytes,
186
+ formatDumpStatsTable
187
+ }
package/lib/exec.js CHANGED
@@ -1,12 +1,71 @@
1
+ import fs from 'fs'
1
2
  import { client as WSClient } from "@live-change/dao-websocket"
2
3
  import lineReader from 'line-reader'
4
+ import {
5
+ parseDumpLine,
6
+ methodName,
7
+ isPut,
8
+ isPutOldLog,
9
+ putTableName,
10
+ putObjectId,
11
+ shouldRunRequest
12
+ } from './dumpCommands.js'
13
+
14
+ function resolveProgressFile(progressFile) {
15
+ if(progressFile === false || progressFile === 'false') return null
16
+ if(progressFile === '' || progressFile == null) return null
17
+ return progressFile
18
+ }
19
+
20
+ function writeProgressFile(path, state) {
21
+ if(!path) return
22
+ const text = [
23
+ `line=${state.line ?? ''}`,
24
+ `inFlightLine=${state.inFlightLine ?? ''}`,
25
+ `inFlightMethod=${state.inFlightMethod ?? ''}`,
26
+ `inFlightTable=${state.inFlightTable ?? ''}`,
27
+ `inFlightId=${state.inFlightId ?? ''}`,
28
+ `updatedAt=${new Date().toISOString()}`
29
+ ].join('\n') + '\n'
30
+ try {
31
+ fs.writeFileSync(path, text)
32
+ } catch(error) {
33
+ console.error('failed to write progress file', path, error.message || error)
34
+ }
35
+ }
3
36
 
4
37
  async function exec(options) {
5
- let { serverUrl, verbose, file, targetDb } = options
38
+ let {
39
+ serverUrl,
40
+ verbose,
41
+ file,
42
+ targetDb,
43
+ progressFile = 'lcdbc-exec.progress',
44
+ fromLine = 0,
45
+ skipIndex = false,
46
+ onlyIndex = false,
47
+ requestTimeout = 0,
48
+ excludeTable
49
+ } = options
6
50
 
7
- let done = false
51
+ if(skipIndex && onlyIndex) {
52
+ throw new Error('--skipIndex and --onlyIndex are mutually exclusive')
53
+ }
54
+
55
+ const excludeTables = new Set(
56
+ Array.isArray(excludeTable)
57
+ ? excludeTable
58
+ : (excludeTable ? [excludeTable] : [])
59
+ )
60
+ const progressPath = resolveProgressFile(progressFile)
61
+ const timeoutMs = requestTimeout > 0 ? requestTimeout : 0
8
62
 
63
+ let done = false
9
64
  let sourceDb
65
+ const startedTables = new Set()
66
+ let lastCompletedLine = 0
67
+ let inFlight = null
68
+ let progressTimer = null
10
69
 
11
70
  const clientPromise = new Promise((resolve, reject) => {
12
71
  const client = new WSClient("commandLine", serverUrl, {
@@ -19,6 +78,16 @@ async function exec(options) {
19
78
  },
20
79
  onDisconnect: () => {
21
80
  if(verbose) console.error("disconnected from server")
81
+ if(progressTimer) clearInterval(progressTimer)
82
+ if(progressPath) {
83
+ writeProgressFile(progressPath, {
84
+ line: lastCompletedLine,
85
+ inFlightLine: inFlight?.line,
86
+ inFlightMethod: inFlight?.method,
87
+ inFlightTable: inFlight?.table,
88
+ inFlightId: inFlight?.id
89
+ })
90
+ }
22
91
  if(!done) {
23
92
  console.error("disconnected before request done")
24
93
  process.exit(1)
@@ -35,6 +104,19 @@ async function exec(options) {
35
104
  const client = await clientPromise
36
105
  let currentPromises = []
37
106
  const maxPromises = 1
107
+ let lineNo = 0
108
+
109
+ if(progressPath) {
110
+ progressTimer = setInterval(() => {
111
+ writeProgressFile(progressPath, {
112
+ line: lastCompletedLine,
113
+ inFlightLine: inFlight?.line,
114
+ inFlightMethod: inFlight?.method,
115
+ inFlightTable: inFlight?.table,
116
+ inFlightId: inFlight?.id
117
+ })
118
+ }, 1000)
119
+ }
38
120
 
39
121
  function nextLine() {
40
122
  return new Promise(function(resolve, reject) {
@@ -44,47 +126,141 @@ async function exec(options) {
44
126
  })
45
127
  })
46
128
  }
47
- while (reader.hasNextLine()) {
48
- const line = await nextLine()
49
- const command = JSON.parse(line)
50
- /* console.log("COMMAND", command.type)
51
- if(command.type == 'request') {
52
- console.log(" ", command.method.join("."), command.parameters.slice(0,2).map(p=>JSON.stringify(p)).join(", "))
53
- } */
54
- switch(command.type) {
55
- case 'request' :
56
- while(currentPromises.length > maxPromises) {
57
- await currentPromises[0]
58
- currentPromises.shift()
59
- }
60
- if(!sourceDb) {
61
- sourceDb = command.parameters[0]
62
- }
63
- if(targetDb) {
64
- if(sourceDb != command.parameters[0])
65
- throw new Error(`source database changed from ${sourceDb} to ${command.parameters[0]}`)
66
- command.parameters[0] = targetDb
67
- }
68
- //console.log("REQUEST", command.method, command.parameters)
69
- currentPromises.push(client.request(command.method, ...command.parameters))
70
- break;
71
- case 'sync' :
72
- await Promise.all(currentPromises)
73
- currentPromises = []
74
- break;
129
+
130
+ function sendRequest(method, parameters) {
131
+ if(timeoutMs) {
132
+ return client.requestWithSettings({ requestTimeout: timeoutMs }, method, ...parameters)
75
133
  }
134
+ return client.request(method, ...parameters)
76
135
  }
77
- reader.close(function(err) {
78
- if (err) throw err
79
- })
80
136
 
81
- await Promise.all(currentPromises)
137
+ async function flushPending() {
138
+ while(currentPromises.length > 0) {
139
+ await currentPromises[0]
140
+ currentPromises.shift()
141
+ }
142
+ }
143
+
144
+ try {
145
+ while (reader.hasNextLine()) {
146
+ const line = await nextLine()
147
+ lineNo++
148
+
149
+ if(fromLine > 0 && lineNo < fromLine) continue
150
+
151
+ const parsed = parseDumpLine(line)
152
+ if(parsed.skip) {
153
+ lastCompletedLine = lineNo
154
+ continue
155
+ }
156
+ if(parsed.error) {
157
+ console.error(`skip bad json line ${lineNo}: ${parsed.preview}`)
158
+ lastCompletedLine = lineNo
159
+ continue
160
+ }
161
+
162
+ const command = parsed.command
163
+ if(command.type === 'sync') {
164
+ await flushPending()
165
+ lastCompletedLine = lineNo
166
+ continue
167
+ }
168
+
169
+ if(command.type !== 'request') {
170
+ lastCompletedLine = lineNo
171
+ continue
172
+ }
173
+
174
+ if(!shouldRunRequest(command, { skipIndex, onlyIndex, excludeTables })) {
175
+ lastCompletedLine = lineNo
176
+ continue
177
+ }
178
+
179
+ while(currentPromises.length > maxPromises) {
180
+ await currentPromises[0]
181
+ currentPromises.shift()
182
+ }
82
183
 
83
- done = true
184
+ if(!sourceDb) {
185
+ sourceDb = command.parameters[0]
186
+ }
187
+ if(targetDb) {
188
+ if(sourceDb != command.parameters[0]) {
189
+ throw new Error(`source database changed from ${sourceDb} to ${command.parameters[0]}`)
190
+ }
191
+ command.parameters[0] = targetDb
192
+ }
84
193
 
85
- client.dispose()
194
+ const method = methodName(command)
195
+ const table = putTableName(command)
196
+ const objectId = putObjectId(command)
86
197
 
198
+ if((isPut(command) || isPutOldLog(command)) && table && !startedTables.has(table)) {
199
+ startedTables.add(table)
200
+ console.error(`table put start: ${table} (line ${lineNo})`)
201
+ }
202
+
203
+ inFlight = {
204
+ line: lineNo,
205
+ method,
206
+ table: table || '',
207
+ id: objectId || ''
208
+ }
209
+
210
+ const requestPromise = sendRequest(command.method, command.parameters)
211
+ .then(() => {
212
+ lastCompletedLine = lineNo
213
+ if(inFlight && inFlight.line === lineNo) inFlight = null
214
+ })
215
+ .catch((error) => {
216
+ if(error === 'timeout' || error?.message === 'timeout') {
217
+ console.error(
218
+ `request timeout after ${timeoutMs}ms: ${method}`
219
+ + (table ? ` table=${table}` : '')
220
+ + (objectId != null ? ` id=${objectId}` : '')
221
+ + ` line=${lineNo}`
222
+ )
223
+ if(progressPath) {
224
+ writeProgressFile(progressPath, {
225
+ line: lastCompletedLine,
226
+ inFlightLine: lineNo,
227
+ inFlightMethod: method,
228
+ inFlightTable: table || '',
229
+ inFlightId: objectId || ''
230
+ })
231
+ }
232
+ if(progressTimer) clearInterval(progressTimer)
233
+ process.exit(1)
234
+ }
235
+ throw error
236
+ })
237
+
238
+ currentPromises.push(requestPromise)
239
+ }
240
+
241
+ await flushPending()
242
+ reader.close(function(closeErr) {
243
+ if (closeErr) throw closeErr
244
+ })
245
+
246
+ done = true
247
+ if(progressTimer) clearInterval(progressTimer)
248
+ if(progressPath) {
249
+ writeProgressFile(progressPath, {
250
+ line: lastCompletedLine,
251
+ inFlightLine: '',
252
+ inFlightMethod: '',
253
+ inFlightTable: '',
254
+ inFlightId: ''
255
+ })
256
+ }
257
+ client.dispose()
258
+ } catch(error) {
259
+ if(progressTimer) clearInterval(progressTimer)
260
+ console.error(error && error.stack ? error.stack : error)
261
+ process.exit(1)
262
+ }
87
263
  })
88
264
  }
89
265
 
90
- export default exec
266
+ export default exec
@@ -0,0 +1,69 @@
1
+ import lineReader from 'line-reader'
2
+ import {
3
+ parseDumpLine,
4
+ shouldRunRequest
5
+ } from './dumpCommands.js'
6
+
7
+ async function filterDump(options) {
8
+ const {
9
+ file = '-',
10
+ excludeTable
11
+ } = options
12
+
13
+ const excludeTables = Array.isArray(excludeTable)
14
+ ? excludeTable
15
+ : (excludeTable ? [excludeTable] : [])
16
+
17
+ if(excludeTables.length === 0) {
18
+ throw new Error('--excludeTable is required at least once')
19
+ }
20
+
21
+ const excludeSet = new Set(excludeTables)
22
+ let lineNo = 0
23
+
24
+ await new Promise((resolve, reject) => {
25
+ lineReader.open(file == '-' ? process.stdin : file, function(err, reader) {
26
+ if(err) return reject(err)
27
+
28
+ function nextLine() {
29
+ return new Promise(function(res, rej) {
30
+ reader.nextLine(function(readErr, line) {
31
+ if(readErr) return rej(readErr)
32
+ res(line)
33
+ })
34
+ })
35
+ }
36
+
37
+ ;(async () => {
38
+ try {
39
+ while(reader.hasNextLine()) {
40
+ const line = await nextLine()
41
+ lineNo++
42
+ const parsed = parseDumpLine(line)
43
+ if(parsed.skip) continue
44
+ if(parsed.error) {
45
+ console.error(`skip bad json line ${lineNo}: ${parsed.preview}`)
46
+ continue
47
+ }
48
+ const command = parsed.command
49
+ if(command.type === 'sync') {
50
+ process.stdout.write(line.endsWith('\n') ? line : line + '\n')
51
+ continue
52
+ }
53
+ if(command.type !== 'request') continue
54
+ if(!shouldRunRequest(command, { excludeTables: excludeSet })) continue
55
+ process.stdout.write(line.endsWith('\n') ? line : line + '\n')
56
+ }
57
+ reader.close(function(closeErr) {
58
+ if(closeErr) return reject(closeErr)
59
+ resolve()
60
+ })
61
+ } catch(error) {
62
+ reject(error)
63
+ }
64
+ })()
65
+ })
66
+ })
67
+ }
68
+
69
+ export default filterDump
@@ -0,0 +1,63 @@
1
+ import lineReader from 'line-reader'
2
+ import {
3
+ parseDumpLine,
4
+ accumulateDumpStats,
5
+ dumpStatsRows,
6
+ sortDumpStats,
7
+ formatDumpStatsTable
8
+ } from './dumpCommands.js'
9
+
10
+ async function statsDump(options) {
11
+ const {
12
+ file = '-',
13
+ sort = 'bytes',
14
+ human = false
15
+ } = options
16
+
17
+ const statsMap = new Map()
18
+ let lineNo = 0
19
+
20
+ await new Promise((resolve, reject) => {
21
+ lineReader.open(file == '-' ? process.stdin : file, function(err, reader) {
22
+ if(err) return reject(err)
23
+
24
+ function nextLine() {
25
+ return new Promise(function(res, rej) {
26
+ reader.nextLine(function(readErr, line) {
27
+ if(readErr) return rej(readErr)
28
+ res(line)
29
+ })
30
+ })
31
+ }
32
+
33
+ ;(async () => {
34
+ try {
35
+ while(reader.hasNextLine()) {
36
+ const line = await nextLine()
37
+ lineNo++
38
+ const parsed = parseDumpLine(line)
39
+ if(parsed.skip) continue
40
+ if(parsed.error) {
41
+ console.error(`skip bad json line ${lineNo}: ${parsed.preview}`)
42
+ continue
43
+ }
44
+ const command = parsed.command
45
+ const lineBytes = Buffer.byteLength(line, 'utf8')
46
+ accumulateDumpStats(statsMap, command, lineBytes)
47
+ }
48
+ reader.close(function(closeErr) {
49
+ if(closeErr) return reject(closeErr)
50
+ resolve()
51
+ })
52
+ } catch(error) {
53
+ reject(error)
54
+ }
55
+ })()
56
+ })
57
+ })
58
+
59
+ const rows = sortDumpStats(dumpStatsRows(statsMap), sort)
60
+ console.log(formatDumpStatsTable(rows, { human }))
61
+ }
62
+
63
+ export default statsDump
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/db-client",
3
- "version": "0.9.225",
3
+ "version": "0.9.227",
4
4
  "description": "Database with observable data for live queries",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -28,11 +28,11 @@
28
28
  },
29
29
  "type": "module",
30
30
  "dependencies": {
31
- "@live-change/dao": "^0.9.225",
32
- "@live-change/dao-websocket": "^0.9.225",
31
+ "@live-change/dao": "^0.9.227",
32
+ "@live-change/dao-websocket": "^0.9.227",
33
33
  "line-reader": "^0.4.0",
34
34
  "websocket": "^1.0.34",
35
35
  "yargs": "^17.7.2"
36
36
  },
37
- "gitHead": "e44dc8b9eefd8b67c40a29b3117781662093c922"
37
+ "gitHead": "7d4dc92dcb9a50381292dded7d8766a3cadbecf9"
38
38
  }
@@ -0,0 +1,106 @@
1
+ import test from 'tape'
2
+ import {
3
+ parseDumpLine,
4
+ methodName,
5
+ isCreateIndex,
6
+ isPut,
7
+ isPutOldLog,
8
+ putTableName,
9
+ putObjectId,
10
+ shouldRunRequest
11
+ } from '../lib/dumpCommands.js'
12
+
13
+ test('parseDumpLine skips empty', (t) => {
14
+ t.deepEqual(parseDumpLine(''), { skip: true })
15
+ t.deepEqual(parseDumpLine(' '), { skip: true })
16
+ t.end()
17
+ })
18
+
19
+ test('parseDumpLine parses request', (t) => {
20
+ const line = JSON.stringify({
21
+ type: 'request',
22
+ method: ['database', 'put'],
23
+ parameters: ['db', 'tableA', { id: 'x1' }]
24
+ })
25
+ const parsed = parseDumpLine(line)
26
+ t.equal(parsed.command.type, 'request')
27
+ t.equal(methodName(parsed.command), 'put')
28
+ t.end()
29
+ })
30
+
31
+ test('parseDumpLine returns error for invalid json', (t) => {
32
+ const parsed = parseDumpLine('Unhandled Rejection at: Promise')
33
+ t.ok(parsed.error)
34
+ t.ok(parsed.preview.startsWith('Unhandled'))
35
+ t.end()
36
+ })
37
+
38
+ test('put helpers extract table and id', (t) => {
39
+ const command = {
40
+ type: 'request',
41
+ method: ['database', 'put'],
42
+ parameters: ['db', 'pageSnapshot', { id: 'abc' }]
43
+ }
44
+ t.ok(isPut(command))
45
+ t.notOk(isPutOldLog(command))
46
+ t.equal(putTableName(command), 'pageSnapshot')
47
+ t.equal(putObjectId(command), 'abc')
48
+ t.end()
49
+ })
50
+
51
+ test('shouldRunRequest skipIndex drops createIndex', (t) => {
52
+ const createIndex = {
53
+ type: 'request',
54
+ method: ['database', 'createIndex'],
55
+ parameters: ['db', 'byName', '()=>{}', {}]
56
+ }
57
+ const put = {
58
+ type: 'request',
59
+ method: ['database', 'put'],
60
+ parameters: ['db', 't', { id: '1' }]
61
+ }
62
+ t.ok(isCreateIndex(createIndex))
63
+ t.notOk(shouldRunRequest(createIndex, { skipIndex: true }))
64
+ t.ok(shouldRunRequest(put, { skipIndex: true }))
65
+ t.ok(shouldRunRequest({ type: 'sync' }, { skipIndex: true }))
66
+ t.end()
67
+ })
68
+
69
+ test('shouldRunRequest onlyIndex keeps createIndex and sync', (t) => {
70
+ const createIndex = {
71
+ type: 'request',
72
+ method: ['database', 'createIndex'],
73
+ parameters: ['db', 'byName', '()=>{}', {}]
74
+ }
75
+ const put = {
76
+ type: 'request',
77
+ method: ['database', 'put'],
78
+ parameters: ['db', 't', { id: '1' }]
79
+ }
80
+ t.ok(shouldRunRequest(createIndex, { onlyIndex: true }))
81
+ t.notOk(shouldRunRequest(put, { onlyIndex: true }))
82
+ t.ok(shouldRunRequest({ type: 'sync' }, { onlyIndex: true }))
83
+ t.end()
84
+ })
85
+
86
+ test('shouldRunRequest excludeTables drops put and putOldLog data', (t) => {
87
+ const put = {
88
+ type: 'request',
89
+ method: ['database', 'put'],
90
+ parameters: ['db', 'Foo', { id: '1' }]
91
+ }
92
+ const putOldLog = {
93
+ type: 'request',
94
+ method: ['database', 'putOldLog'],
95
+ parameters: ['db', 'Bar', { id: '2' }]
96
+ }
97
+ const createTable = {
98
+ type: 'request',
99
+ method: ['database', 'createTable'],
100
+ parameters: ['db', 'Foo']
101
+ }
102
+ t.notOk(shouldRunRequest(put, { excludeTables: ['Foo'] }))
103
+ t.notOk(shouldRunRequest(putOldLog, { excludeTables: ['Bar'] }))
104
+ t.ok(shouldRunRequest(createTable, { excludeTables: ['Foo'] }))
105
+ t.end()
106
+ })
@@ -0,0 +1,94 @@
1
+ import test from 'tape'
2
+ import {
3
+ accumulateDumpStats,
4
+ dumpStatsRows,
5
+ sortDumpStats,
6
+ formatHumanBytes,
7
+ formatDumpStatsTable
8
+ } from '../lib/dumpCommands.js'
9
+
10
+ test('accumulateDumpStats sums put entries and bytes', (t) => {
11
+ const map = new Map()
12
+ const put = {
13
+ type: 'request',
14
+ method: ['database', 'put'],
15
+ parameters: ['db', 'Foo', { id: '1' }]
16
+ }
17
+ accumulateDumpStats(map, put, 100)
18
+ accumulateDumpStats(map, put, 50)
19
+ const rows = dumpStatsRows(map)
20
+ t.equal(rows.length, 1)
21
+ t.deepEqual(rows[0], { kind: 'table', name: 'Foo', entries: 2, bytes: 150 })
22
+ t.end()
23
+ })
24
+
25
+ test('accumulateDumpStats separates table and log with same name', (t) => {
26
+ const map = new Map()
27
+ accumulateDumpStats(map, {
28
+ type: 'request',
29
+ method: ['database', 'put'],
30
+ parameters: ['db', 'Same', { id: '1' }]
31
+ }, 10)
32
+ accumulateDumpStats(map, {
33
+ type: 'request',
34
+ method: ['database', 'putOldLog'],
35
+ parameters: ['db', 'Same', { id: '2' }]
36
+ }, 20)
37
+ const rows = sortDumpStats(dumpStatsRows(map), 'bytes')
38
+ t.equal(rows.length, 2)
39
+ t.equal(rows[0].kind, 'log')
40
+ t.equal(rows[0].bytes, 20)
41
+ t.equal(rows[1].kind, 'table')
42
+ t.equal(rows[1].bytes, 10)
43
+ t.end()
44
+ })
45
+
46
+ test('accumulateDumpStats ignores createTable sync and non-request', (t) => {
47
+ const map = new Map()
48
+ accumulateDumpStats(map, {
49
+ type: 'request',
50
+ method: ['database', 'createTable'],
51
+ parameters: ['db', 'Foo']
52
+ }, 999)
53
+ accumulateDumpStats(map, { type: 'sync' }, 50)
54
+ accumulateDumpStats(map, null, 10)
55
+ t.equal(dumpStatsRows(map).length, 0)
56
+ t.end()
57
+ })
58
+
59
+ test('sortDumpStats by entries descending', (t) => {
60
+ const rows = [
61
+ { kind: 'table', name: 'A', entries: 1, bytes: 1000 },
62
+ { kind: 'table', name: 'B', entries: 5, bytes: 10 },
63
+ { kind: 'log', name: 'C', entries: 3, bytes: 100 }
64
+ ]
65
+ const byEntries = sortDumpStats(rows, 'entries')
66
+ t.deepEqual(byEntries.map((r) => r.name), ['B', 'C', 'A'])
67
+ const byBytes = sortDumpStats(rows, 'bytes')
68
+ t.deepEqual(byBytes.map((r) => r.name), ['A', 'C', 'B'])
69
+ t.end()
70
+ })
71
+
72
+ test('formatHumanBytes', (t) => {
73
+ t.equal(formatHumanBytes(500), '500')
74
+ t.equal(formatHumanBytes(2048), '2.00KiB')
75
+ t.equal(formatHumanBytes(1024 * 1024), '1.00MiB')
76
+ t.end()
77
+ })
78
+
79
+ test('formatDumpStatsTable includes TOTAL and human sizes', (t) => {
80
+ const rows = [
81
+ { kind: 'log', name: 'BigLog', entries: 2, bytes: 2048 },
82
+ { kind: 'table', name: 'Small', entries: 1, bytes: 10 }
83
+ ]
84
+ const plain = formatDumpStatsTable(rows, { human: false })
85
+ t.ok(plain.includes('TOTAL'))
86
+ t.ok(plain.includes('BigLog'))
87
+ t.ok(plain.includes('2058'))
88
+ t.ok(plain.includes('3'))
89
+
90
+ const human = formatDumpStatsTable(rows, { human: true })
91
+ t.ok(human.includes('2.00KiB'))
92
+ t.ok(human.includes('TOTAL'))
93
+ t.end()
94
+ })