From 45d9eb03838da46a4dab141310e0b3616400e799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 8 Sep 2022 10:53:00 +0200 Subject: [PATCH 01/19] feat: add ActivityLog backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/activity-log.js | 79 ++++++++++++++++++++++++++++++++++ main/test/activity-log.test.js | 64 +++++++++++++++++++++++++++ main/test/smoke.test.js | 9 ---- main/test/test-helpers.js | 46 ++++++++++++++++++++ main/typings.d.ts | 14 ++++++ 5 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 main/activity-log.js create mode 100644 main/test/activity-log.test.js delete mode 100644 main/test/smoke.test.js create mode 100644 main/test/test-helpers.js diff --git a/main/activity-log.js b/main/activity-log.js new file mode 100644 index 000000000..677778913 --- /dev/null +++ b/main/activity-log.js @@ -0,0 +1,79 @@ +'use strict' + +const Store = require('electron-store') +const configStore = new Store({ + name: 'activity-log' +}) + +class ActivityLog { + #entries + #lastId + + constructor () { + this.#entries = loadStoredEntries() + this.#lastId = this.#entries + // We are storing ids as strings to allow us to switch to GUIDs in the future if needed + // When looking for the max id used, we need to cast strings to numbers. + .map(e => +e.id) + // Find the maximum id or return 0 when no events were recorded yet + .reduce((max, it) => it > max ? it : max, 0) + } + + /** + * @param {import('./typings').ActivityEvent} args + * @returns {import('./typings').ActivityEntry} + */ + recordEvent ({ source, type, message }) { + const nextId = ++this.#lastId + /** @type {import('./typings').ActivityEntry} */ + const entry = { + id: '' + nextId, + timestamp: Date.now(), + source, + type, + message + } + this.#entries.push(entry) + this.#save() + // Clone the data to prevent the caller from accidentally changing our store + return clone(entry) + } + + getAllEntries () { + // Clone the data to prevent the caller from accidentally changing our store + return this.#entries.map(clone) + } + + static reset () { + const self = new ActivityLog() + self.#entries = [] + self.#save() + } + + #save () { + configStore.set('events', this.#entries) + } +} + +/** + * TODO: use `structuredClone` (available from Node.js v17/v18, not supported by Electron yet) + * + * @template {object} T + * @param {T} data + * @returns {T} + */ +function clone (data) { + return { ...data } +} + +/** + * @returns {import('./typings').ActivityEntry[]} + */ +function loadStoredEntries () { + // A workaround to fix false TypeScript errors + return /** @type {any} */(configStore.get('events', [])) +} + +module.exports = { + ActivityLog +} diff --git a/main/test/activity-log.test.js b/main/test/activity-log.test.js new file mode 100644 index 000000000..2ff7d2809 --- /dev/null +++ b/main/test/activity-log.test.js @@ -0,0 +1,64 @@ +'use strict' + +const assert = require('assert').strict +const { ActivityLog } = require('../activity-log') +const { assertTimestampIsCloseToNow, pickProps } = require('./test-helpers') + +describe('ActivityLog', function () { + beforeEach(function () { return ActivityLog.reset() }) + + it('record events and assign them timestamp and id ', function () { + const activityLog = new ActivityLog() + const entryCreated = activityLog.recordEvent(givenActivity({ + source: 'Station', + type: 'info', + message: 'Hello world!' + })) + + assert.strictEqual(activityLog.getAllEntries().length, 1) + assert.deepStrictEqual(entryCreated, activityLog.getAllEntries()[0]) + + const { timestamp, ...entry } = activityLog.getAllEntries()[0] + assert.deepStrictEqual(entry, { + id: '1', + source: 'Station', + type: 'info', + message: 'Hello world!' + }) + + assertTimestampIsCloseToNow(timestamp, 'event.timestamp') + }) + + it('assigns unique ids', function () { + const activityLog = new ActivityLog() + activityLog.recordEvent(givenActivity({ message: 'one' })) + activityLog.recordEvent(givenActivity({ message: 'two' })) + assert.deepStrictEqual(activityLog.getAllEntries().map(it => pickProps(it, 'id', 'message')), [ + { id: '1', message: 'one' }, + { id: '2', message: 'two' } + ]) + }) + + it('preserves events across restarts', function () { + new ActivityLog().recordEvent(givenActivity({ message: 'first run' })) + const activityLog = new ActivityLog() + activityLog.recordEvent(givenActivity({ message: 'second run' })) + assert.deepStrictEqual(activityLog.getAllEntries().map(it => pickProps(it, 'id', 'message')), [ + { id: '1', message: 'first run' }, + { id: '2', message: 'second run' } + ]) + }) +}) + +/** + * @param {Partial} [props] + * @returns {import('../typings').ActivityEvent} + */ +function givenActivity (props) { + return { + source: 'Station', + type: 'info', + message: 'some message', + ...props + } +} diff --git a/main/test/smoke.test.js b/main/test/smoke.test.js deleted file mode 100644 index 7bfc07deb..000000000 --- a/main/test/smoke.test.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -const assert = require('node:assert') - -describe('smoke test', function () { - it('works', function () { - assert.strictEqual(true, true) - }) -}) diff --git a/main/test/test-helpers.js b/main/test/test-helpers.js new file mode 100644 index 000000000..970e01fa2 --- /dev/null +++ b/main/test/test-helpers.js @@ -0,0 +1,46 @@ +'use strict' + +const assert = require('assert').strict + +/** @template {object} Obj + * @template {keyof Obj} Props + * @param {Obj} data + * @param {Props[]} propNames + * @returns {Pick} + */ +function pickProps (data, ...propNames) { + /** @type {(string | number | symbol)[]} */ + const names = propNames + /** @type {any} */ + const result = {} + for (const key in data) { + if (names.includes(key)) { + result[key] = data[key] + } + } + return result +} + +/** + * @param {string} valueDescription + * @param {number} actualValue + * @param {number} maxDeltaInMs + */ +function assertTimestampIsCloseToNow (actualValue, valueDescription = 'timestamp', maxDeltaInMs = 50) { + const now = Date.now() + const delta = Math.abs(actualValue - now) + + const nowStr = new Date(now).toISOString() + const actualStr = new Date(actualValue).toISOString() + + assert( + delta < maxDeltaInMs, + `Expected ${valueDescription} to be within ${maxDeltaInMs}ms from ${nowStr}, ` + + `but got ${actualStr} instead (differs by ${delta}ms).` + ) +} + +module.exports = { + pickProps, + assertTimestampIsCloseToNow +} diff --git a/main/typings.d.ts b/main/typings.d.ts index fca704a04..6bdf979a7 100644 --- a/main/typings.d.ts +++ b/main/typings.d.ts @@ -1,3 +1,17 @@ +export type ActivitySource = 'Station' | 'Saturn'; +export type ActivityEventType = 'info' | 'error'; + +export interface ActivityEvent { + type: ActivityEventType; + source: ActivitySource; + message: string; +} + +export interface ActivityEntry extends ActivityEvent { + id: string; + timestamp: number; +} + export interface Context { showUI: () => void loadWebUIFromDist: import('electron-serve').loadURL From 733f4e260b493d68850ff81f947604259b1b5f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 8 Sep 2022 12:40:38 +0200 Subject: [PATCH 02/19] feat: record ActivityLog entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - When the Station starts/stops - When the Saturn Module starts/stops - INFO & ERROR logs from Saturn Signed-off-by: Miroslav Bajtoš --- main/index.js | 26 ++++++++++++++++++++++++++ main/saturn-node.js | 36 +++++++++++++++++++++++++++++++----- main/typings.d.ts | 1 + 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/main/index.js b/main/index.js index d4463d0c3..27dc9d58c 100644 --- a/main/index.js +++ b/main/index.js @@ -12,9 +12,17 @@ const saturnNode = require('./saturn-node') const { setupIpcMain } = require('./ipc') const { setupAppMenu } = require('./app-menu') +const { ActivityLog } = require('./activity-log') + const inTest = (process.env.NODE_ENV === 'test') function handleError (/** @type {any} */ err) { + ctx.recordActivity({ + source: 'Station', + type: 'error', + message: `Station failed to start: ${err.message || err}` + }) + log.error(err) dialog.showErrorBox('Error occured', err.stack ?? err.message ?? err) } @@ -37,11 +45,16 @@ if (!app.requestSingleInstanceLock() && !inTest) { /** @type {import('./typings').Context} */ const ctx = { + recordActivity, manualCheckForUpdates: () => { throw new Error('never get here') }, showUI: () => { throw new Error('never get here') }, loadWebUIFromDist: serve({ directory: path.resolve(__dirname, '../renderer/dist') }) } +process.on('exit', () => { + ctx.recordActivity({ source: 'Station', type: 'info', message: 'Station stopped.' }) +}) + async function run () { try { await app.whenReady() @@ -58,10 +71,23 @@ async function run () { await setupUpdater(ctx) await setupIpcMain() + ctx.recordActivity({ source: 'Station', type: 'info', message: 'Station started.' }) + await saturnNode.setup(ctx) } catch (e) { handleError(e) } } +const activityLog = new ActivityLog() + +/** + * @param {import('./typings').ActivityEvent} event + */ +function recordActivity (event) { + const entry = activityLog.recordEvent(event) + console.log('[ACTIVITY]', entry) + // TODO: send IPC event +} + run() diff --git a/main/saturn-node.js b/main/saturn-node.js index b83ab6aa7..ee1fcc65f 100644 --- a/main/saturn-node.js +++ b/main/saturn-node.js @@ -31,7 +31,7 @@ const ConfigKeys = { let filAddress = /** @type {string | undefined} */ (configStore.get(ConfigKeys.FilAddress)) -async function setup (/** @type {import('./typings').Context} */ _ctx) { +async function setup (/** @type {import('./typings').Context} */ ctx) { console.log('Using Saturn L2 Node binary: %s', saturnBinaryPath) const stat = await fs.stat(saturnBinaryPath) @@ -41,7 +41,7 @@ async function setup (/** @type {import('./typings').Context} */ _ctx) { if (!childProcess) return stop() }) - await start() + await start(ctx) } function getSaturnBinaryPath () { @@ -55,7 +55,7 @@ function getSaturnBinaryPath () { : path.resolve(__dirname, '..', 'build', 'saturn', `l2node-${process.platform}-${arch}`, name) } -async function start () { +async function start (/** @type {import('./typings').Context} */ ctx) { if (!filAddress) { console.info('Saturn node requires FIL address. Please configure it in the Station UI.') return @@ -88,6 +88,7 @@ async function start () { stdout.on('data', (/** @type {string} */ data) => { forwardChunkFromSaturn(data, console.log) appendToChildLog(data) + handleActivityLogs(ctx, data) }) stderr.setEncoding('utf-8') @@ -111,6 +112,8 @@ async function start () { console.log('Saturn node is up and ready (Web URL: %s)', webUrl) ready = true stdout.off('data', readyHandler) + + ctx.recordActivity({ source: 'Saturn', type: 'info', message: 'Saturn module started.' }) resolve() } } @@ -131,6 +134,7 @@ async function start () { const msg = `Saturn node exited ${reason}` console.log(msg) appendToChildLog(msg) + ctx.recordActivity({ source: 'Saturn', type: 'info', message: msg }) ready = false }) @@ -141,9 +145,11 @@ async function start () { setTimeout(500) ]) } catch (err) { - const msg = err instanceof Error ? err.message : '' + err - appendToChildLog(`Cannot start Saturn node: ${msg}`) + const errorMsg = err instanceof Error ? err.message : '' + err + const message = `Cannot start Saturn node: ${errorMsg}` + appendToChildLog(message) console.error('Cannot start Saturn node:', err) + ctx.recordActivity({ source: 'Saturn', type: 'error', message }) } } @@ -211,6 +217,26 @@ function appendToChildLog (text) { .join('') } +/** + * @param {import('./typings').Context} ctx + * @param {string} text + */ +function handleActivityLogs (ctx, text) { + text + .trimEnd() + .split(/\n/g) + .forEach(line => { + const m = line.match(/^(INFO|ERROR): (.*)$/) + if (!m) return + + ctx.recordActivity({ + source: 'Saturn', + type: /** @type {any} */(m[1].toLowerCase()), + message: m[2] + }) + }) +} + module.exports = { setup, start, diff --git a/main/typings.d.ts b/main/typings.d.ts index 6bdf979a7..6c9f25ed9 100644 --- a/main/typings.d.ts +++ b/main/typings.d.ts @@ -13,6 +13,7 @@ export interface ActivityEntry extends ActivityEvent { } export interface Context { + recordActivity(event: ActivityEvent): void; showUI: () => void loadWebUIFromDist: import('electron-serve').loadURL manualCheckForUpdates: () => void From ac0abd9cf6069e05ac151832fb219f65d0d435e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 8 Sep 2022 15:11:00 +0200 Subject: [PATCH 03/19] feat: expose ActivityLog to renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/index.js | 18 ++++++++++++++---- main/ipc.js | 10 ++++++++-- main/preload.js | 9 +++++++++ main/typings.d.ts | 2 ++ main/ui.js | 10 ++++++++++ renderer/src/main.tsx | 9 +++++++++ renderer/src/typings.d.ts | 5 +++++ 7 files changed, 57 insertions(+), 6 deletions(-) diff --git a/main/index.js b/main/index.js index 27dc9d58c..0091b47e7 100644 --- a/main/index.js +++ b/main/index.js @@ -9,13 +9,19 @@ const setupUI = require('./ui') const setupTray = require('./tray') const setupUpdater = require('./updater') const saturnNode = require('./saturn-node') -const { setupIpcMain } = require('./ipc') +const { setupIpcMain, ipcMainEvents } = require('./ipc') const { setupAppMenu } = require('./app-menu') const { ActivityLog } = require('./activity-log') +const { ipcMain } = require('electron/main') const inTest = (process.env.NODE_ENV === 'test') +if (!app.isPackaged && !inTest) { + // Do not preserve old Activity entries in development mode + ActivityLog.reset() +} + function handleError (/** @type {any} */ err) { ctx.recordActivity({ source: 'Station', @@ -45,6 +51,7 @@ if (!app.requestSingleInstanceLock() && !inTest) { /** @type {import('./typings').Context} */ const ctx = { + getAllActivityLogEntries, recordActivity, manualCheckForUpdates: () => { throw new Error('never get here') }, showUI: () => { throw new Error('never get here') }, @@ -69,7 +76,7 @@ async function run () { await setupAppMenu(ctx) await setupUI(ctx) await setupUpdater(ctx) - await setupIpcMain() + await setupIpcMain(ctx) ctx.recordActivity({ source: 'Station', type: 'info', message: 'Station started.' }) @@ -86,8 +93,11 @@ const activityLog = new ActivityLog() */ function recordActivity (event) { const entry = activityLog.recordEvent(event) - console.log('[ACTIVITY]', entry) - // TODO: send IPC event + ipcMain.emit(ipcMainEvents.ACTIVITY_LOGGED, entry) +} + +function getAllActivityLogEntries () { + return activityLog.getAllEntries() } run() diff --git a/main/ipc.js b/main/ipc.js index b082884d7..55e6d527b 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -6,14 +6,16 @@ const saturnNode = require('./saturn-node') const stationConfig = require('./station-config') const ipcMainEvents = Object.freeze({ + ACTIVITY_LOGGED: 'station:activity-logged', + UPDATE_CHECK_STARTED: 'station:update-check:started', UPDATE_CHECK_FINISHED: 'station:update-check:finished' }) -function setupIpcMain () { +function setupIpcMain (/** @type {import('./typings').Context} */ ctx) { ipcMain.handle('saturn:isRunning', saturnNode.isRunning) ipcMain.handle('saturn:isReady', saturnNode.isReady) - ipcMain.handle('saturn:start', saturnNode.start) + ipcMain.handle('saturn:start', _event => saturnNode.start(ctx)) ipcMain.handle('saturn:stop', saturnNode.stop) ipcMain.handle('saturn:getLog', saturnNode.getLog) ipcMain.handle('saturn:getWebUrl', saturnNode.getWebUrl) @@ -26,6 +28,10 @@ function setupIpcMain () { ipcMain.handle('station:setOnboardingCompleted', (_event) => stationConfig.setOnboardingCompleted()) ipcMain.handle('station:getUserConsent', stationConfig.getUserConsent) ipcMain.handle('station:setUserConsent', (_event, consent) => stationConfig.setUserConsent(consent)) + + ipcMain.handle('station:getActivityLog', (_event, _args) => { + return ctx.getAllActivityLogEntries() + }) } module.exports = { diff --git a/main/preload.js b/main/preload.js index 3e5385853..5a3c9045f 100644 --- a/main/preload.js +++ b/main/preload.js @@ -3,6 +3,15 @@ const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electron', { + getActivityLog: () => ipcRenderer.invoke('station:getActivityLog'), + + /** + * @param {(activityEntry: import('./typings').ActivityEntry) => void} callback + */ + onActivityLogged (callback) { + ipcRenderer.on('station:activity-logged', (_event, entry) => callback(entry)) + }, + saturnNode: { start: () => ipcRenderer.invoke('saturn:start'), stop: () => ipcRenderer.invoke('saturn:stop'), diff --git a/main/typings.d.ts b/main/typings.d.ts index 6c9f25ed9..7774d53e8 100644 --- a/main/typings.d.ts +++ b/main/typings.d.ts @@ -14,6 +14,8 @@ export interface ActivityEntry extends ActivityEvent { export interface Context { recordActivity(event: ActivityEvent): void; + getAllActivityLogEntries(): ActivityEntry[]; + showUI: () => void loadWebUIFromDist: import('electron-serve').loadURL manualCheckForUpdates: () => void diff --git a/main/ui.js b/main/ui.js index ce9ba7cad..234bd30de 100644 --- a/main/ui.js +++ b/main/ui.js @@ -4,6 +4,8 @@ const path = require('path') const { BrowserWindow, app, screen } = require('electron') const store = require('./store') +const { ipcMain } = require('electron/main') +const { ipcMainEvents } = require('./ipc') /** * @param {import('./typings').Context} ctx @@ -66,8 +68,16 @@ module.exports = async function (ctx) { } ui.once('ready-to-show', ctx.showUI) + const onNewActivity = (/** @type {unknown[]} */ ...args) => { + if (ui.isDestroyed()) return // This happens when quitting the entire app + ui.webContents.send(ipcMainEvents.ACTIVITY_LOGGED, ...args) + } + ipcMain.on(ipcMainEvents.ACTIVITY_LOGGED, onNewActivity) + // Don't exit when window is closed (Quit only via Tray icon menu) ui.on('close', (event) => { + console.log('---CLOSE WINDOW---') + ipcMain.removeListener(ipcMainEvents.ACTIVITY_LOGGED, onNewActivity) event.preventDefault() ui.hide() if (app.dock) app.dock.hide() diff --git a/renderer/src/main.tsx b/renderer/src/main.tsx index 9eed4e29f..baca83773 100644 --- a/renderer/src/main.tsx +++ b/renderer/src/main.tsx @@ -14,3 +14,12 @@ ReactDOM.createRoot( ) + +window.electron.getActivityLog().then(log => { + console.log('GOT INITIAL ACTIVITY LOG') + log.forEach(entry => console.log('[ACTIVITY] %j', entry)) +}) + +window.electron.onActivityLogged(entry => { + console.log('[ACTIVITY] %j', entry) +}) diff --git a/renderer/src/typings.d.ts b/renderer/src/typings.d.ts index 58ce4f238..19cebd7a2 100644 --- a/renderer/src/typings.d.ts +++ b/renderer/src/typings.d.ts @@ -1,6 +1,11 @@ +import { ActivityEntry } from '../main/typings' + export declare global { interface Window { electron: { + getActivityLog(): Promise, + onActivityLogged(callback: (activityEntry: ActivityEntry) => void), + saturnNode: { start:() => Promise, stop: () => Promise, From aad1f6ef7024ad388084cc86ded9c11f8a69fd60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 8 Sep 2022 15:37:41 +0200 Subject: [PATCH 04/19] fixup! address Julian comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/activity-log.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/main/activity-log.js b/main/activity-log.js index 677778913..c91e93e1c 100644 --- a/main/activity-log.js +++ b/main/activity-log.js @@ -1,7 +1,7 @@ 'use strict' const Store = require('electron-store') -const configStore = new Store({ +const activityLogStore = new Store({ name: 'activity-log' }) @@ -11,12 +11,7 @@ class ActivityLog { constructor () { this.#entries = loadStoredEntries() - this.#lastId = this.#entries - // We are storing ids as strings to allow us to switch to GUIDs in the future if needed - // When looking for the max id used, we need to cast strings to numbers. - .map(e => +e.id) - // Find the maximum id or return 0 when no events were recorded yet - .reduce((max, it) => it > max ? it : max, 0) + this.#lastId = Number(this.#entries.at(-1)?.id ?? 0) } /** @@ -33,15 +28,17 @@ class ActivityLog { type, message } + // Freeze the data to prevent ActivityLog users from accidentally changing our store + Object.freeze(entry) + this.#entries.push(entry) this.#save() - // Clone the data to prevent the caller from accidentally changing our store - return clone(entry) + return entry } getAllEntries () { - // Clone the data to prevent the caller from accidentally changing our store - return this.#entries.map(clone) + // Clone the array to prevent the caller from accidentally changing our store + return [...this.#entries] } static reset () { @@ -51,7 +48,7 @@ class ActivityLog { } #save () { - configStore.set('events', this.#entries) + activityLogStore.set('events', this.#entries) } } @@ -71,7 +68,7 @@ function clone (data) { */ function loadStoredEntries () { // A workaround to fix false TypeScript errors - return /** @type {any} */(configStore.get('events', [])) + return /** @type {any} */(activityLogStore.get('events', [])) } module.exports = { From 780b762b246b731fad1bed8d18fe1a5d91f993ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 8 Sep 2022 15:38:38 +0200 Subject: [PATCH 05/19] fixup! use String(val) instead of '' + val MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/activity-log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/activity-log.js b/main/activity-log.js index c91e93e1c..45bd7ded9 100644 --- a/main/activity-log.js +++ b/main/activity-log.js @@ -22,7 +22,7 @@ class ActivityLog { const nextId = ++this.#lastId /** @type {import('./typings').ActivityEntry} */ const entry = { - id: '' + nextId, + id: String(nextId), timestamp: Date.now(), source, type, From 610e8cf67fb3bfc11e5c77a8c7b8a3723341be63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 8 Sep 2022 15:40:39 +0200 Subject: [PATCH 06/19] fixup! remove unused `clone()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/activity-log.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/main/activity-log.js b/main/activity-log.js index 45bd7ded9..22fde902f 100644 --- a/main/activity-log.js +++ b/main/activity-log.js @@ -52,17 +52,6 @@ class ActivityLog { } } -/** - * TODO: use `structuredClone` (available from Node.js v17/v18, not supported by Electron yet) - * - * @template {object} T - * @param {T} data - * @returns {T} - */ -function clone (data) { - return { ...data } -} - /** * @returns {import('./typings').ActivityEntry[]} */ From 3e97d6a303b7e93fecf724cd1e650bcc14fa528c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 8 Sep 2022 15:40:48 +0200 Subject: [PATCH 07/19] fixup! import types via JSDoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/activity-log.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/main/activity-log.js b/main/activity-log.js index 22fde902f..e2b8a4aac 100644 --- a/main/activity-log.js +++ b/main/activity-log.js @@ -1,5 +1,8 @@ 'use strict' +/** @typedef {import('./typings').ActivityEvent} ActivityEvent */ +/** @typedef {import('./typings').ActivityEntry} ActivityEntry */ + const Store = require('electron-store') const activityLogStore = new Store({ name: 'activity-log' @@ -15,12 +18,12 @@ class ActivityLog { } /** - * @param {import('./typings').ActivityEvent} args - * @returns {import('./typings').ActivityEntry} + * @param {ActivityEvent} args + * @returns {ActivityEntry} */ recordEvent ({ source, type, message }) { const nextId = ++this.#lastId - /** @type {import('./typings').ActivityEntry} */ + /** @type {ActivityEntry} */ const entry = { id: String(nextId), timestamp: Date.now(), @@ -53,7 +56,7 @@ class ActivityLog { } /** - * @returns {import('./typings').ActivityEntry[]} + * @returns {ActivityEntry[]} */ function loadStoredEntries () { // A workaround to fix false TypeScript errors From 4b8ddab2977deb442a96bccf498ffdb6af930508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 8 Sep 2022 15:47:37 +0200 Subject: [PATCH 08/19] feat: limit ActivityLog size to 100 items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/activity-log.js | 5 +++++ main/test/activity-log.test.js | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/main/activity-log.js b/main/activity-log.js index e2b8a4aac..eed1ad243 100644 --- a/main/activity-log.js +++ b/main/activity-log.js @@ -35,6 +35,11 @@ class ActivityLog { Object.freeze(entry) this.#entries.push(entry) + + if (this.#entries.length > 100) { + // Delete the oldest entry to keep ActivityLog at constant size + this.#entries.shift() + } this.#save() return entry } diff --git a/main/test/activity-log.test.js b/main/test/activity-log.test.js index 2ff7d2809..f068b84bf 100644 --- a/main/test/activity-log.test.js +++ b/main/test/activity-log.test.js @@ -48,6 +48,20 @@ describe('ActivityLog', function () { { id: '2', message: 'second run' } ]) }) + + it('limits the log to the most recent 50 entries', function () { + this.timeout(10000) + + const log = new ActivityLog() + for (let i = 0; i < 110; i++) { + log.recordEvent(givenActivity({ message: `event ${i}` })) + } + const entries = log.getAllEntries() + assert.deepStrictEqual( + [entries.at(0)?.message, entries.at(-1)?.message], + ['event 10', 'event 109'] + ) + }) }) /** From 03594122c705257da0cd7e930c40e997704867eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 8 Sep 2022 15:53:54 +0200 Subject: [PATCH 09/19] fixup! remove forgotten console log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/ui.js | 1 - 1 file changed, 1 deletion(-) diff --git a/main/ui.js b/main/ui.js index 234bd30de..b0faf2d3e 100644 --- a/main/ui.js +++ b/main/ui.js @@ -76,7 +76,6 @@ module.exports = async function (ctx) { // Don't exit when window is closed (Quit only via Tray icon menu) ui.on('close', (event) => { - console.log('---CLOSE WINDOW---') ipcMain.removeListener(ipcMainEvents.ACTIVITY_LOGGED, onNewActivity) event.preventDefault() ui.hide() From 4fd20c419e7d3d584ae374e3c379aab4fce968fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 13 Sep 2022 10:30:51 +0200 Subject: [PATCH 10/19] fixup! add more typedefs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/index.js | 4 +++- main/ipc.js | 4 +++- main/saturn-node.js | 8 +++++--- main/test/activity-log.test.js | 6 ++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/main/index.js b/main/index.js index 0091b47e7..24a64ca61 100644 --- a/main/index.js +++ b/main/index.js @@ -15,6 +15,8 @@ const { setupAppMenu } = require('./app-menu') const { ActivityLog } = require('./activity-log') const { ipcMain } = require('electron/main') +/** @typedef {import('./typings').ActivityEvent} ActivityEvent */ + const inTest = (process.env.NODE_ENV === 'test') if (!app.isPackaged && !inTest) { @@ -89,7 +91,7 @@ async function run () { const activityLog = new ActivityLog() /** - * @param {import('./typings').ActivityEvent} event + * @param {ActivityEvent} event */ function recordActivity (event) { const entry = activityLog.recordEvent(event) diff --git a/main/ipc.js b/main/ipc.js index 55e6d527b..b84942713 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -5,6 +5,8 @@ const { ipcMain } = require('electron') const saturnNode = require('./saturn-node') const stationConfig = require('./station-config') +/** @typedef {import('./typings').Context} Context */ + const ipcMainEvents = Object.freeze({ ACTIVITY_LOGGED: 'station:activity-logged', @@ -12,7 +14,7 @@ const ipcMainEvents = Object.freeze({ UPDATE_CHECK_FINISHED: 'station:update-check:finished' }) -function setupIpcMain (/** @type {import('./typings').Context} */ ctx) { +function setupIpcMain (/** @type {Context} */ ctx) { ipcMain.handle('saturn:isRunning', saturnNode.isRunning) ipcMain.handle('saturn:isReady', saturnNode.isReady) ipcMain.handle('saturn:start', _event => saturnNode.start(ctx)) diff --git a/main/saturn-node.js b/main/saturn-node.js index ee1fcc65f..79a1c54b6 100644 --- a/main/saturn-node.js +++ b/main/saturn-node.js @@ -12,6 +12,8 @@ const Store = require('electron-store') const consts = require('./consts') const configStore = new Store() +/** @typedef {import('./typings').Context} Context */ + const saturnBinaryPath = getSaturnBinaryPath() /** @type {import('execa').ExecaChildProcess | null} */ @@ -31,7 +33,7 @@ const ConfigKeys = { let filAddress = /** @type {string | undefined} */ (configStore.get(ConfigKeys.FilAddress)) -async function setup (/** @type {import('./typings').Context} */ ctx) { +async function setup (/** @type {Context} */ ctx) { console.log('Using Saturn L2 Node binary: %s', saturnBinaryPath) const stat = await fs.stat(saturnBinaryPath) @@ -55,7 +57,7 @@ function getSaturnBinaryPath () { : path.resolve(__dirname, '..', 'build', 'saturn', `l2node-${process.platform}-${arch}`, name) } -async function start (/** @type {import('./typings').Context} */ ctx) { +async function start (/** @type {Context} */ ctx) { if (!filAddress) { console.info('Saturn node requires FIL address. Please configure it in the Station UI.') return @@ -218,7 +220,7 @@ function appendToChildLog (text) { } /** - * @param {import('./typings').Context} ctx + * @param {Context} ctx * @param {string} text */ function handleActivityLogs (ctx, text) { diff --git a/main/test/activity-log.test.js b/main/test/activity-log.test.js index f068b84bf..cffb859c4 100644 --- a/main/test/activity-log.test.js +++ b/main/test/activity-log.test.js @@ -4,6 +4,8 @@ const assert = require('assert').strict const { ActivityLog } = require('../activity-log') const { assertTimestampIsCloseToNow, pickProps } = require('./test-helpers') +/** @typedef {import('../typings').ActivityEvent} ActivityEvent */ + describe('ActivityLog', function () { beforeEach(function () { return ActivityLog.reset() }) @@ -65,8 +67,8 @@ describe('ActivityLog', function () { }) /** - * @param {Partial} [props] - * @returns {import('../typings').ActivityEvent} + * @param {Partial} [props] + * @returns {ActivityEvent} */ function givenActivity (props) { return { From d6eb2144332cbb08f8663e6e8f6e6dd92d295d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 13 Sep 2022 10:34:25 +0200 Subject: [PATCH 11/19] fixup! extract isDev variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main/index.js b/main/index.js index 24a64ca61..5f4353aa5 100644 --- a/main/index.js +++ b/main/index.js @@ -18,8 +18,9 @@ const { ipcMain } = require('electron/main') /** @typedef {import('./typings').ActivityEvent} ActivityEvent */ const inTest = (process.env.NODE_ENV === 'test') +const isDev = !app.isPackaged && !inTest -if (!app.isPackaged && !inTest) { +if (isDev) { // Do not preserve old Activity entries in development mode ActivityLog.reset() } From 32713506cc3dec83fec1aea51c2d43223b0ade6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 13 Sep 2022 10:37:21 +0200 Subject: [PATCH 12/19] fixup! fix deregistration of the IPC listener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/ui.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main/ui.js b/main/ui.js index b0faf2d3e..21db8cc45 100644 --- a/main/ui.js +++ b/main/ui.js @@ -69,14 +69,12 @@ module.exports = async function (ctx) { ui.once('ready-to-show', ctx.showUI) const onNewActivity = (/** @type {unknown[]} */ ...args) => { - if (ui.isDestroyed()) return // This happens when quitting the entire app ui.webContents.send(ipcMainEvents.ACTIVITY_LOGGED, ...args) } ipcMain.on(ipcMainEvents.ACTIVITY_LOGGED, onNewActivity) // Don't exit when window is closed (Quit only via Tray icon menu) ui.on('close', (event) => { - ipcMain.removeListener(ipcMainEvents.ACTIVITY_LOGGED, onNewActivity) event.preventDefault() ui.hide() if (app.dock) app.dock.hide() @@ -86,6 +84,7 @@ module.exports = async function (ctx) { // that were added to keep app running when the UI is closed. // (Without this, the app would run forever and/or fail to update) app.on('before-quit', () => { + ipcMain.removeListener(ipcMainEvents.ACTIVITY_LOGGED, onNewActivity) ui.removeAllListeners('close') devServer?.close() }) From 7e2dcad05a0ad460295cfef58d14f9e6d18fbafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 13 Sep 2022 10:53:28 +0200 Subject: [PATCH 13/19] fixup! rework activity streaming to renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/index.js | 19 +++++++++++++++---- main/ipc.js | 4 ++-- main/preload.js | 2 +- main/typings.d.ts | 2 +- renderer/src/main.tsx | 9 ++++----- renderer/src/typings.d.ts | 2 +- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/main/index.js b/main/index.js index 5f4353aa5..30a53d4fe 100644 --- a/main/index.js +++ b/main/index.js @@ -15,7 +15,8 @@ const { setupAppMenu } = require('./app-menu') const { ActivityLog } = require('./activity-log') const { ipcMain } = require('electron/main') -/** @typedef {import('./typings').ActivityEvent} ActivityEvent */ +/** @typedef {import('./typings').ActivityEvent} ActivityEvent */ +/** @typedef {import('./typings').ActivityEntry} ActivityEntry */ const inTest = (process.env.NODE_ENV === 'test') const isDev = !app.isPackaged && !inTest @@ -54,7 +55,7 @@ if (!app.requestSingleInstanceLock() && !inTest) { /** @type {import('./typings').Context} */ const ctx = { - getAllActivityLogEntries, + resumeActivityStream, recordActivity, manualCheckForUpdates: () => { throw new Error('never get here') }, showUI: () => { throw new Error('never get here') }, @@ -90,17 +91,27 @@ async function run () { } const activityLog = new ActivityLog() +let isActivityStreamFlowing = false /** * @param {ActivityEvent} event */ function recordActivity (event) { const entry = activityLog.recordEvent(event) + if (isActivityStreamFlowing) emitActivity(entry) +} + +/** + * @param {ActivityEntry} entry + */ +function emitActivity (entry) { ipcMain.emit(ipcMainEvents.ACTIVITY_LOGGED, entry) } -function getAllActivityLogEntries () { - return activityLog.getAllEntries() +function resumeActivityStream () { + isActivityStreamFlowing = true + const existingEntries = activityLog.getAllEntries() + for (const it of existingEntries) emitActivity(it) } run() diff --git a/main/ipc.js b/main/ipc.js index b84942713..bb5bea565 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -31,8 +31,8 @@ function setupIpcMain (/** @type {Context} */ ctx) { ipcMain.handle('station:getUserConsent', stationConfig.getUserConsent) ipcMain.handle('station:setUserConsent', (_event, consent) => stationConfig.setUserConsent(consent)) - ipcMain.handle('station:getActivityLog', (_event, _args) => { - return ctx.getAllActivityLogEntries() + ipcMain.handle('station:resumeActivityStream', (_event, _args) => { + return ctx.resumeActivityStream() }) } diff --git a/main/preload.js b/main/preload.js index 5a3c9045f..38f940f2a 100644 --- a/main/preload.js +++ b/main/preload.js @@ -3,7 +3,7 @@ const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electron', { - getActivityLog: () => ipcRenderer.invoke('station:getActivityLog'), + resumeActivityStream: () => ipcRenderer.invoke('station:resumeActivityStream'), /** * @param {(activityEntry: import('./typings').ActivityEntry) => void} callback diff --git a/main/typings.d.ts b/main/typings.d.ts index 7774d53e8..28a76d65a 100644 --- a/main/typings.d.ts +++ b/main/typings.d.ts @@ -14,7 +14,7 @@ export interface ActivityEntry extends ActivityEvent { export interface Context { recordActivity(event: ActivityEvent): void; - getAllActivityLogEntries(): ActivityEntry[]; + resumeActivityStream(): void; showUI: () => void loadWebUIFromDist: import('electron-serve').loadURL diff --git a/renderer/src/main.tsx b/renderer/src/main.tsx index baca83773..47d13ea1a 100644 --- a/renderer/src/main.tsx +++ b/renderer/src/main.tsx @@ -15,11 +15,10 @@ ReactDOM.createRoot( ) -window.electron.getActivityLog().then(log => { - console.log('GOT INITIAL ACTIVITY LOG') - log.forEach(entry => console.log('[ACTIVITY] %j', entry)) -}) - window.electron.onActivityLogged(entry => { console.log('[ACTIVITY] %j', entry) }) + +window.electron.resumeActivityStream().then(() => { + console.log('ACTIVITY STREAM RESUMED') +}) diff --git a/renderer/src/typings.d.ts b/renderer/src/typings.d.ts index 19cebd7a2..b228f9e72 100644 --- a/renderer/src/typings.d.ts +++ b/renderer/src/typings.d.ts @@ -3,7 +3,7 @@ import { ActivityEntry } from '../main/typings' export declare global { interface Window { electron: { - getActivityLog(): Promise, + resumeActivityStream(): Promise, onActivityLogged(callback: (activityEntry: ActivityEntry) => void), saturnNode: { From d8e010f7bd2a7fa84b8b8558ec407ac6c4c54703 Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Thu, 15 Sep 2022 13:18:48 +0200 Subject: [PATCH 14/19] Refactor activity log types (#127) * prototype activity log layout * fix types * refactor types * refactor names * refactor variable names * fix sorting bug * docs --- main/activity-log.js | 33 ++++++++++++++++----------------- main/index.js | 18 +++++++++--------- main/preload.js | 4 ++-- main/test/activity-log.test.js | 32 ++++++++++++++++---------------- main/typings.d.ts | 17 ++++++++++------- renderer/src/main.tsx | 19 +++++++++++++++++-- renderer/src/typings.d.ts | 6 +++--- 7 files changed, 73 insertions(+), 56 deletions(-) diff --git a/main/activity-log.js b/main/activity-log.js index eed1ad243..646861482 100644 --- a/main/activity-log.js +++ b/main/activity-log.js @@ -1,47 +1,46 @@ 'use strict' -/** @typedef {import('./typings').ActivityEvent} ActivityEvent */ -/** @typedef {import('./typings').ActivityEntry} ActivityEntry */ +/** @typedef {import('./typings').Activity} Activity */ +/** @typedef {import('./typings').RecordActivityOptions} RecordActivityOptions */ const Store = require('electron-store') +const crypto = require('node:crypto') + const activityLogStore = new Store({ name: 'activity-log' }) class ActivityLog { #entries - #lastId constructor () { this.#entries = loadStoredEntries() - this.#lastId = Number(this.#entries.at(-1)?.id ?? 0) } /** - * @param {ActivityEvent} args - * @returns {ActivityEntry} + * @param {RecordActivityOptions} args + * @returns {Activity} */ - recordEvent ({ source, type, message }) { - const nextId = ++this.#lastId - /** @type {ActivityEntry} */ - const entry = { - id: String(nextId), + recordActivity ({ source, type, message }) { + /** @type {Activity} */ + const activity = { + id: crypto.randomUUID(), timestamp: Date.now(), source, type, message } // Freeze the data to prevent ActivityLog users from accidentally changing our store - Object.freeze(entry) + Object.freeze(activity) - this.#entries.push(entry) + this.#entries.push(activity) if (this.#entries.length > 100) { - // Delete the oldest entry to keep ActivityLog at constant size + // Delete the oldest activity to keep ActivityLog at constant size this.#entries.shift() } this.#save() - return entry + return activity } getAllEntries () { @@ -56,12 +55,12 @@ class ActivityLog { } #save () { - activityLogStore.set('events', this.#entries) + activityLogStore.set('activities', this.#entries) } } /** - * @returns {ActivityEntry[]} + * @returns {Activity[]} */ function loadStoredEntries () { // A workaround to fix false TypeScript errors diff --git a/main/index.js b/main/index.js index 30a53d4fe..d7d0ae525 100644 --- a/main/index.js +++ b/main/index.js @@ -15,8 +15,8 @@ const { setupAppMenu } = require('./app-menu') const { ActivityLog } = require('./activity-log') const { ipcMain } = require('electron/main') -/** @typedef {import('./typings').ActivityEvent} ActivityEvent */ -/** @typedef {import('./typings').ActivityEntry} ActivityEntry */ +/** @typedef {import('./typings').Activity} Activity */ +/** @typedef {import('./typings').RecordActivityOptions} RecordActivityOptions */ const inTest = (process.env.NODE_ENV === 'test') const isDev = !app.isPackaged && !inTest @@ -94,18 +94,18 @@ const activityLog = new ActivityLog() let isActivityStreamFlowing = false /** - * @param {ActivityEvent} event + * @param {RecordActivityOptions} opts */ -function recordActivity (event) { - const entry = activityLog.recordEvent(event) - if (isActivityStreamFlowing) emitActivity(entry) +function recordActivity (opts) { + const activity = activityLog.recordActivity(opts) + if (isActivityStreamFlowing) emitActivity(activity) } /** - * @param {ActivityEntry} entry + * @param {Activity} activity */ -function emitActivity (entry) { - ipcMain.emit(ipcMainEvents.ACTIVITY_LOGGED, entry) +function emitActivity (activity) { + ipcMain.emit(ipcMainEvents.ACTIVITY_LOGGED, activity) } function resumeActivityStream () { diff --git a/main/preload.js b/main/preload.js index 38f940f2a..564f3e753 100644 --- a/main/preload.js +++ b/main/preload.js @@ -6,10 +6,10 @@ contextBridge.exposeInMainWorld('electron', { resumeActivityStream: () => ipcRenderer.invoke('station:resumeActivityStream'), /** - * @param {(activityEntry: import('./typings').ActivityEntry) => void} callback + * @param {(Activity: import('./typings').Activity) => void} callback */ onActivityLogged (callback) { - ipcRenderer.on('station:activity-logged', (_event, entry) => callback(entry)) + ipcRenderer.on('station:activity-logged', (_event, activity) => callback(activity)) }, saturnNode: { diff --git a/main/test/activity-log.test.js b/main/test/activity-log.test.js index cffb859c4..57c082dc6 100644 --- a/main/test/activity-log.test.js +++ b/main/test/activity-log.test.js @@ -4,47 +4,47 @@ const assert = require('assert').strict const { ActivityLog } = require('../activity-log') const { assertTimestampIsCloseToNow, pickProps } = require('./test-helpers') -/** @typedef {import('../typings').ActivityEvent} ActivityEvent */ +/** @typedef {import('../typings').RecordActivityOptions} RecordActivityOptions */ describe('ActivityLog', function () { beforeEach(function () { return ActivityLog.reset() }) - it('record events and assign them timestamp and id ', function () { + it('record activities and assign them timestamp and id ', function () { const activityLog = new ActivityLog() - const entryCreated = activityLog.recordEvent(givenActivity({ + const activityCreated = activityLog.recordActivity(givenActivity({ source: 'Station', type: 'info', message: 'Hello world!' })) assert.strictEqual(activityLog.getAllEntries().length, 1) - assert.deepStrictEqual(entryCreated, activityLog.getAllEntries()[0]) + assert.deepStrictEqual(activityCreated, activityLog.getAllEntries()[0]) - const { timestamp, ...entry } = activityLog.getAllEntries()[0] - assert.deepStrictEqual(entry, { + const { timestamp, ...activity } = activityLog.getAllEntries()[0] + assert.deepStrictEqual(activity, { id: '1', source: 'Station', type: 'info', message: 'Hello world!' }) - assertTimestampIsCloseToNow(timestamp, 'event.timestamp') + assertTimestampIsCloseToNow(timestamp, 'activity.timestamp') }) it('assigns unique ids', function () { const activityLog = new ActivityLog() - activityLog.recordEvent(givenActivity({ message: 'one' })) - activityLog.recordEvent(givenActivity({ message: 'two' })) + activityLog.recordActivity(givenActivity({ message: 'one' })) + activityLog.recordActivity(givenActivity({ message: 'two' })) assert.deepStrictEqual(activityLog.getAllEntries().map(it => pickProps(it, 'id', 'message')), [ { id: '1', message: 'one' }, { id: '2', message: 'two' } ]) }) - it('preserves events across restarts', function () { - new ActivityLog().recordEvent(givenActivity({ message: 'first run' })) + it('preserves activities across restarts', function () { + new ActivityLog().recordActivity(givenActivity({ message: 'first run' })) const activityLog = new ActivityLog() - activityLog.recordEvent(givenActivity({ message: 'second run' })) + activityLog.recordActivity(givenActivity({ message: 'second run' })) assert.deepStrictEqual(activityLog.getAllEntries().map(it => pickProps(it, 'id', 'message')), [ { id: '1', message: 'first run' }, { id: '2', message: 'second run' } @@ -56,19 +56,19 @@ describe('ActivityLog', function () { const log = new ActivityLog() for (let i = 0; i < 110; i++) { - log.recordEvent(givenActivity({ message: `event ${i}` })) + log.recordActivity(givenActivity({ message: `activity ${i}` })) } const entries = log.getAllEntries() assert.deepStrictEqual( [entries.at(0)?.message, entries.at(-1)?.message], - ['event 10', 'event 109'] + ['activity 10', 'activity 109'] ) }) }) /** - * @param {Partial} [props] - * @returns {ActivityEvent} + * @param {Partial} [props] + * @returns {RecordActivityOptions} */ function givenActivity (props) { return { diff --git a/main/typings.d.ts b/main/typings.d.ts index 28a76d65a..63497868f 100644 --- a/main/typings.d.ts +++ b/main/typings.d.ts @@ -1,19 +1,22 @@ export type ActivitySource = 'Station' | 'Saturn'; -export type ActivityEventType = 'info' | 'error'; +export type ActivityType = 'info' | 'error'; -export interface ActivityEvent { - type: ActivityEventType; +export interface Activity { + id: string; + timestamp: number; + type: ActivityType; source: ActivitySource; message: string; } -export interface ActivityEntry extends ActivityEvent { - id: string; - timestamp: number; +export interface RecordActivityOptions { + type: ActivityType; + source: ActivitySource; + message: string; } export interface Context { - recordActivity(event: ActivityEvent): void; + recordActivity(activity: RecordActivityOptions): void; resumeActivityStream(): void; showUI: () => void diff --git a/renderer/src/main.tsx b/renderer/src/main.tsx index 47d13ea1a..abcf311d2 100644 --- a/renderer/src/main.tsx +++ b/renderer/src/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import App from './App' import './index.css' +import { Activity } from '../../main/typings' ReactDOM.createRoot( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -15,8 +16,22 @@ ReactDOM.createRoot( ) -window.electron.onActivityLogged(entry => { - console.log('[ACTIVITY] %j', entry) +const activities: Activity[] = [] + +window.electron.onActivityLogged(activity => { + activities.push(activity) + // In case two activities were recorded in the same millisecond, fall back to + // sorting by their IDs, which are guaranteed to be unique and therefore + // provide a stable sorting. + activities.sort((a, b) => { + return a.timestamp !== b.timestamp + ? b.timestamp - a.timestamp + : a.id.localeCompare(b.id) + }) + if (activities.length > 100) activities.shift() + + console.log('[ACTIVITY] %j', activity) + console.log('[ACTIVITIES]', activities) }) window.electron.resumeActivityStream().then(() => { diff --git a/renderer/src/typings.d.ts b/renderer/src/typings.d.ts index b228f9e72..d8baebd0c 100644 --- a/renderer/src/typings.d.ts +++ b/renderer/src/typings.d.ts @@ -1,10 +1,10 @@ -import { ActivityEntry } from '../main/typings' +import { Activity } from '../main/typings' export declare global { interface Window { electron: { - resumeActivityStream(): Promise, - onActivityLogged(callback: (activityEntry: ActivityEntry) => void), + resumeActivityStream(): Promise, + onActivityLogged(callback: (activity: Activity) => void), saturnNode: { start:() => Promise, From cf3e3f106f24249091e87b23e4d91282f88249d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 15 Sep 2022 13:24:23 +0200 Subject: [PATCH 15/19] fix: fix eslint issue in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/test/activity-log.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/test/activity-log.test.js b/main/test/activity-log.test.js index 57c082dc6..c9c91e538 100644 --- a/main/test/activity-log.test.js +++ b/main/test/activity-log.test.js @@ -51,7 +51,7 @@ describe('ActivityLog', function () { ]) }) - it('limits the log to the most recent 50 entries', function () { + it('limits the log to the most recent 50 entries', /** @this {Mocha.Test} */ function () { this.timeout(10000) const log = new ActivityLog() From cde1d463a2469767cf10bc482af2eee832422caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 15 Sep 2022 14:00:28 +0200 Subject: [PATCH 16/19] refactor: with Julian MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julian Gruber Signed-off-by: Miroslav Bajtoš --- main/activity-log.js | 6 +++--- main/index.js | 8 ++++---- main/ipc.js | 4 ++-- main/preload.js | 2 +- main/test/activity-log.test.js | 14 +++++++------- main/typings.d.ts | 10 +++------- renderer/src/main.tsx | 4 ++-- renderer/src/typings.d.ts | 2 +- 8 files changed, 23 insertions(+), 27 deletions(-) diff --git a/main/activity-log.js b/main/activity-log.js index 646861482..95a452aa5 100644 --- a/main/activity-log.js +++ b/main/activity-log.js @@ -1,7 +1,7 @@ 'use strict' /** @typedef {import('./typings').Activity} Activity */ -/** @typedef {import('./typings').RecordActivityOptions} RecordActivityOptions */ +/** @typedef {import('./typings').RecordActivityArgs} RecordActivityArgs */ const Store = require('electron-store') const crypto = require('node:crypto') @@ -18,10 +18,10 @@ class ActivityLog { } /** - * @param {RecordActivityOptions} args + * @param {RecordActivityArgs} args * @returns {Activity} */ - recordActivity ({ source, type, message }) { + record ({ source, type, message }) { /** @type {Activity} */ const activity = { id: crypto.randomUUID(), diff --git a/main/index.js b/main/index.js index d7d0ae525..9078c6404 100644 --- a/main/index.js +++ b/main/index.js @@ -16,7 +16,7 @@ const { ActivityLog } = require('./activity-log') const { ipcMain } = require('electron/main') /** @typedef {import('./typings').Activity} Activity */ -/** @typedef {import('./typings').RecordActivityOptions} RecordActivityOptions */ +/** @typedef {import('./typings').RecordActivityArgs} RecordActivityOptions */ const inTest = (process.env.NODE_ENV === 'test') const isDev = !app.isPackaged && !inTest @@ -55,7 +55,7 @@ if (!app.requestSingleInstanceLock() && !inTest) { /** @type {import('./typings').Context} */ const ctx = { - resumeActivityStream, + startActivityStream, recordActivity, manualCheckForUpdates: () => { throw new Error('never get here') }, showUI: () => { throw new Error('never get here') }, @@ -97,7 +97,7 @@ let isActivityStreamFlowing = false * @param {RecordActivityOptions} opts */ function recordActivity (opts) { - const activity = activityLog.recordActivity(opts) + const activity = activityLog.record(opts) if (isActivityStreamFlowing) emitActivity(activity) } @@ -108,7 +108,7 @@ function emitActivity (activity) { ipcMain.emit(ipcMainEvents.ACTIVITY_LOGGED, activity) } -function resumeActivityStream () { +function startActivityStream () { isActivityStreamFlowing = true const existingEntries = activityLog.getAllEntries() for (const it of existingEntries) emitActivity(it) diff --git a/main/ipc.js b/main/ipc.js index bb5bea565..edffee657 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -31,8 +31,8 @@ function setupIpcMain (/** @type {Context} */ ctx) { ipcMain.handle('station:getUserConsent', stationConfig.getUserConsent) ipcMain.handle('station:setUserConsent', (_event, consent) => stationConfig.setUserConsent(consent)) - ipcMain.handle('station:resumeActivityStream', (_event, _args) => { - return ctx.resumeActivityStream() + ipcMain.handle('station:startActivityStream', (_event, _args) => { + return ctx.startActivityStream() }) } diff --git a/main/preload.js b/main/preload.js index 564f3e753..5e2b53264 100644 --- a/main/preload.js +++ b/main/preload.js @@ -3,7 +3,7 @@ const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electron', { - resumeActivityStream: () => ipcRenderer.invoke('station:resumeActivityStream'), + startActivityStream: () => ipcRenderer.invoke('station:startActivityStream'), /** * @param {(Activity: import('./typings').Activity) => void} callback diff --git a/main/test/activity-log.test.js b/main/test/activity-log.test.js index c9c91e538..d9c38591f 100644 --- a/main/test/activity-log.test.js +++ b/main/test/activity-log.test.js @@ -4,14 +4,14 @@ const assert = require('assert').strict const { ActivityLog } = require('../activity-log') const { assertTimestampIsCloseToNow, pickProps } = require('./test-helpers') -/** @typedef {import('../typings').RecordActivityOptions} RecordActivityOptions */ +/** @typedef {import('../typings').RecordActivityArgs} RecordActivityOptions */ describe('ActivityLog', function () { beforeEach(function () { return ActivityLog.reset() }) it('record activities and assign them timestamp and id ', function () { const activityLog = new ActivityLog() - const activityCreated = activityLog.recordActivity(givenActivity({ + const activityCreated = activityLog.record(givenActivity({ source: 'Station', type: 'info', message: 'Hello world!' @@ -33,8 +33,8 @@ describe('ActivityLog', function () { it('assigns unique ids', function () { const activityLog = new ActivityLog() - activityLog.recordActivity(givenActivity({ message: 'one' })) - activityLog.recordActivity(givenActivity({ message: 'two' })) + activityLog.record(givenActivity({ message: 'one' })) + activityLog.record(givenActivity({ message: 'two' })) assert.deepStrictEqual(activityLog.getAllEntries().map(it => pickProps(it, 'id', 'message')), [ { id: '1', message: 'one' }, { id: '2', message: 'two' } @@ -42,9 +42,9 @@ describe('ActivityLog', function () { }) it('preserves activities across restarts', function () { - new ActivityLog().recordActivity(givenActivity({ message: 'first run' })) + new ActivityLog().record(givenActivity({ message: 'first run' })) const activityLog = new ActivityLog() - activityLog.recordActivity(givenActivity({ message: 'second run' })) + activityLog.record(givenActivity({ message: 'second run' })) assert.deepStrictEqual(activityLog.getAllEntries().map(it => pickProps(it, 'id', 'message')), [ { id: '1', message: 'first run' }, { id: '2', message: 'second run' } @@ -56,7 +56,7 @@ describe('ActivityLog', function () { const log = new ActivityLog() for (let i = 0; i < 110; i++) { - log.recordActivity(givenActivity({ message: `activity ${i}` })) + log.record(givenActivity({ message: `activity ${i}` })) } const entries = log.getAllEntries() assert.deepStrictEqual( diff --git a/main/typings.d.ts b/main/typings.d.ts index 63497868f..586e4658e 100644 --- a/main/typings.d.ts +++ b/main/typings.d.ts @@ -9,15 +9,11 @@ export interface Activity { message: string; } -export interface RecordActivityOptions { - type: ActivityType; - source: ActivitySource; - message: string; -} +export type RecordActivityArgs = Omit; export interface Context { - recordActivity(activity: RecordActivityOptions): void; - resumeActivityStream(): void; + recordActivity(activity: RecordActivityArgs): void; + startActivityStream(): void; showUI: () => void loadWebUIFromDist: import('electron-serve').loadURL diff --git a/renderer/src/main.tsx b/renderer/src/main.tsx index abcf311d2..adb60f54d 100644 --- a/renderer/src/main.tsx +++ b/renderer/src/main.tsx @@ -1,9 +1,9 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' +import { Activity } from '../../main/typings' import App from './App' import './index.css' -import { Activity } from '../../main/typings' ReactDOM.createRoot( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -34,6 +34,6 @@ window.electron.onActivityLogged(activity => { console.log('[ACTIVITIES]', activities) }) -window.electron.resumeActivityStream().then(() => { +window.electron.startActivityStream().then(() => { console.log('ACTIVITY STREAM RESUMED') }) diff --git a/renderer/src/typings.d.ts b/renderer/src/typings.d.ts index d8baebd0c..91b6da5df 100644 --- a/renderer/src/typings.d.ts +++ b/renderer/src/typings.d.ts @@ -3,7 +3,7 @@ import { Activity } from '../main/typings' export declare global { interface Window { electron: { - resumeActivityStream(): Promise, + startActivityStream(): Promise, onActivityLogged(callback: (activity: Activity) => void), saturnNode: { From cd4157d1699dad53ddc7af9522932f1cc75f070a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 15 Sep 2022 14:34:00 +0200 Subject: [PATCH 17/19] feat: renderer integration & component Co-authored-by: Julian Gruber --- main/index.js | 35 +++++++------------------ main/ipc.js | 4 +-- main/preload.js | 11 ++++++-- main/typings.d.ts | 2 +- renderer/src/App.tsx | 3 ++- renderer/src/components/ActivityLog.tsx | 33 +++++++++++++++++++++++ renderer/src/main.tsx | 23 ---------------- renderer/src/typings.d.ts | 4 +-- 8 files changed, 57 insertions(+), 58 deletions(-) create mode 100644 renderer/src/components/ActivityLog.tsx diff --git a/main/index.js b/main/index.js index 9078c6404..dc1836334 100644 --- a/main/index.js +++ b/main/index.js @@ -53,10 +53,17 @@ if (!app.requestSingleInstanceLock() && !inTest) { app.quit() } +const activityLog = new ActivityLog() + /** @type {import('./typings').Context} */ const ctx = { - startActivityStream, - recordActivity, + getAllActivities: () => activityLog.getAllEntries(), + + recordActivity: (args) => { + activityLog.record(args) + ipcMain.emit(ipcMainEvents.ACTIVITY_LOGGED, activityLog.getAllEntries()) + }, + manualCheckForUpdates: () => { throw new Error('never get here') }, showUI: () => { throw new Error('never get here') }, loadWebUIFromDist: serve({ directory: path.resolve(__dirname, '../renderer/dist') }) @@ -90,28 +97,4 @@ async function run () { } } -const activityLog = new ActivityLog() -let isActivityStreamFlowing = false - -/** - * @param {RecordActivityOptions} opts - */ -function recordActivity (opts) { - const activity = activityLog.record(opts) - if (isActivityStreamFlowing) emitActivity(activity) -} - -/** - * @param {Activity} activity - */ -function emitActivity (activity) { - ipcMain.emit(ipcMainEvents.ACTIVITY_LOGGED, activity) -} - -function startActivityStream () { - isActivityStreamFlowing = true - const existingEntries = activityLog.getAllEntries() - for (const it of existingEntries) emitActivity(it) -} - run() diff --git a/main/ipc.js b/main/ipc.js index edffee657..bf0338ba3 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -31,9 +31,7 @@ function setupIpcMain (/** @type {Context} */ ctx) { ipcMain.handle('station:getUserConsent', stationConfig.getUserConsent) ipcMain.handle('station:setUserConsent', (_event, consent) => stationConfig.setUserConsent(consent)) - ipcMain.handle('station:startActivityStream', (_event, _args) => { - return ctx.startActivityStream() - }) + ipcMain.handle('station:getAllActivities', (_event, _args) => ctx.getAllActivities()) } module.exports = { diff --git a/main/preload.js b/main/preload.js index 5e2b53264..e32fc9212 100644 --- a/main/preload.js +++ b/main/preload.js @@ -3,13 +3,20 @@ const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electron', { - startActivityStream: () => ipcRenderer.invoke('station:startActivityStream'), + getAllActivities: () => ipcRenderer.invoke('station:getAllActivities'), /** * @param {(Activity: import('./typings').Activity) => void} callback */ onActivityLogged (callback) { - ipcRenderer.on('station:activity-logged', (_event, activity) => callback(activity)) + /** @type {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} */ + const listener = (_event, activities) => callback(activities) + + ipcRenderer.on('station:activity-logged', listener) + + return function unsubscribe () { + ipcRenderer.removeListener('station:activity-logged', listener) + } }, saturnNode: { diff --git a/main/typings.d.ts b/main/typings.d.ts index 586e4658e..ea2fe02cc 100644 --- a/main/typings.d.ts +++ b/main/typings.d.ts @@ -13,7 +13,7 @@ export type RecordActivityArgs = Omit; export interface Context { recordActivity(activity: RecordActivityArgs): void; - startActivityStream(): void; + getAllActivities(): void; showUI: () => void loadWebUIFromDist: import('electron-serve').loadURL diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index abc22da10..8cb7e5ac3 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx @@ -3,6 +3,7 @@ import { Link, Route, Routes } from 'react-router-dom' import './App.css' +import { ActivityLog } from './components/ActivityLog' import Saturn from './Saturn' function App (): JSX.Element { @@ -28,9 +29,9 @@ function Home (): JSX.Element { return (
-
🛰

Welcome to Filecoin Station

Saturn >>

+
) } diff --git a/renderer/src/components/ActivityLog.tsx b/renderer/src/components/ActivityLog.tsx new file mode 100644 index 000000000..08cc1802f --- /dev/null +++ b/renderer/src/components/ActivityLog.tsx @@ -0,0 +1,33 @@ +import React, { useEffect, useState } from 'react' +import { Activity } from '../../../main/typings' + +export const ActivityLog : React.FC = () => { + const [activities, setActivities] = useState([]) + + useEffect(() => { + (async () => { + const newActivities = await window.electron.getAllActivities() + setActivities(newActivities) + })() + }, []) + + useEffect(() => { + const unsubscribe = window.electron.onActivityLogged(allActivities => { + setActivities(allActivities) + }) + return unsubscribe() + }, []) + + // TODO: add CSS styling :) + const style: React.CSSProperties = { + listStyle: 'none', + textAlign: 'left', + fontSize: '1rem', + fontFamily: 'monospace', + border: '1px solid white', + padding: '1em' + } + return
    + {activities.map(activity =>
  • {activity.message}
  • )} +
+} diff --git a/renderer/src/main.tsx b/renderer/src/main.tsx index adb60f54d..9eed4e29f 100644 --- a/renderer/src/main.tsx +++ b/renderer/src/main.tsx @@ -1,7 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' -import { Activity } from '../../main/typings' import App from './App' import './index.css' @@ -15,25 +14,3 @@ ReactDOM.createRoot( ) - -const activities: Activity[] = [] - -window.electron.onActivityLogged(activity => { - activities.push(activity) - // In case two activities were recorded in the same millisecond, fall back to - // sorting by their IDs, which are guaranteed to be unique and therefore - // provide a stable sorting. - activities.sort((a, b) => { - return a.timestamp !== b.timestamp - ? b.timestamp - a.timestamp - : a.id.localeCompare(b.id) - }) - if (activities.length > 100) activities.shift() - - console.log('[ACTIVITY] %j', activity) - console.log('[ACTIVITIES]', activities) -}) - -window.electron.startActivityStream().then(() => { - console.log('ACTIVITY STREAM RESUMED') -}) diff --git a/renderer/src/typings.d.ts b/renderer/src/typings.d.ts index 91b6da5df..ea360a920 100644 --- a/renderer/src/typings.d.ts +++ b/renderer/src/typings.d.ts @@ -3,8 +3,8 @@ import { Activity } from '../main/typings' export declare global { interface Window { electron: { - startActivityStream(): Promise, - onActivityLogged(callback: (activity: Activity) => void), + getAllActivities(): Promise, + onActivityLogged(callback: (allActivities: Activity[]) => void), saturnNode: { start:() => Promise, From 6bff5be26dbf49fb82b496504b16ff869c297bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 15 Sep 2022 14:46:53 +0200 Subject: [PATCH 18/19] fixup! more tweaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš Co-authored-by: Julian Gruber --- main/activity-log.js | 7 +++---- main/index.js | 9 ++++----- main/test/activity-log.test.js | 2 +- renderer/src/components/ActivityLog.tsx | 14 +++++++++----- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/main/activity-log.js b/main/activity-log.js index 95a452aa5..ab9e49b0d 100644 --- a/main/activity-log.js +++ b/main/activity-log.js @@ -48,10 +48,9 @@ class ActivityLog { return [...this.#entries] } - static reset () { - const self = new ActivityLog() - self.#entries = [] - self.#save() + reset () { + this.#entries = [] + this.#save() } #save () { diff --git a/main/index.js b/main/index.js index dc1836334..41c854f36 100644 --- a/main/index.js +++ b/main/index.js @@ -21,11 +21,6 @@ const { ipcMain } = require('electron/main') const inTest = (process.env.NODE_ENV === 'test') const isDev = !app.isPackaged && !inTest -if (isDev) { - // Do not preserve old Activity entries in development mode - ActivityLog.reset() -} - function handleError (/** @type {any} */ err) { ctx.recordActivity({ source: 'Station', @@ -54,6 +49,10 @@ if (!app.requestSingleInstanceLock() && !inTest) { } const activityLog = new ActivityLog() +if (isDev) { + // Do not preserve old Activity entries in development mode + activityLog.reset() +} /** @type {import('./typings').Context} */ const ctx = { diff --git a/main/test/activity-log.test.js b/main/test/activity-log.test.js index d9c38591f..12d1ce285 100644 --- a/main/test/activity-log.test.js +++ b/main/test/activity-log.test.js @@ -7,7 +7,7 @@ const { assertTimestampIsCloseToNow, pickProps } = require('./test-helpers') /** @typedef {import('../typings').RecordActivityArgs} RecordActivityOptions */ describe('ActivityLog', function () { - beforeEach(function () { return ActivityLog.reset() }) + beforeEach(function () { return new ActivityLog().reset() }) it('record activities and assign them timestamp and id ', function () { const activityLog = new ActivityLog() diff --git a/renderer/src/components/ActivityLog.tsx b/renderer/src/components/ActivityLog.tsx index 08cc1802f..92e176bae 100644 --- a/renderer/src/components/ActivityLog.tsx +++ b/renderer/src/components/ActivityLog.tsx @@ -15,10 +15,10 @@ export const ActivityLog : React.FC = () => { const unsubscribe = window.electron.onActivityLogged(allActivities => { setActivities(allActivities) }) - return unsubscribe() + return unsubscribe }, []) - // TODO: add CSS styling :) + // TODO: add proper CSS styling :) const style: React.CSSProperties = { listStyle: 'none', textAlign: 'left', @@ -27,7 +27,11 @@ export const ActivityLog : React.FC = () => { border: '1px solid white', padding: '1em' } - return
    - {activities.map(activity =>
  • {activity.message}
  • )} -
+ + return <> +

Activity Log

+
    + {activities.map(activity =>
  • {activity.message}
  • ).reverse()} +
+ } From 739b4a6e56bc7ecac3a11066aaf8e3a82b775a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 15 Sep 2022 15:13:16 +0200 Subject: [PATCH 19/19] fixup! fix store loading and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- main/activity-log.js | 2 +- main/test/activity-log.test.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/main/activity-log.js b/main/activity-log.js index ab9e49b0d..0e9a6f188 100644 --- a/main/activity-log.js +++ b/main/activity-log.js @@ -63,7 +63,7 @@ class ActivityLog { */ function loadStoredEntries () { // A workaround to fix false TypeScript errors - return /** @type {any} */(activityLogStore.get('events', [])) + return /** @type {any} */(activityLogStore.get('activities', [])) } module.exports = { diff --git a/main/test/activity-log.test.js b/main/test/activity-log.test.js index 12d1ce285..bfc5fbc26 100644 --- a/main/test/activity-log.test.js +++ b/main/test/activity-log.test.js @@ -7,7 +7,9 @@ const { assertTimestampIsCloseToNow, pickProps } = require('./test-helpers') /** @typedef {import('../typings').RecordActivityArgs} RecordActivityOptions */ describe('ActivityLog', function () { - beforeEach(function () { return new ActivityLog().reset() }) + beforeEach(function () { + return new ActivityLog().reset() + }) it('record activities and assign them timestamp and id ', function () { const activityLog = new ActivityLog() @@ -20,14 +22,14 @@ describe('ActivityLog', function () { assert.strictEqual(activityLog.getAllEntries().length, 1) assert.deepStrictEqual(activityCreated, activityLog.getAllEntries()[0]) - const { timestamp, ...activity } = activityLog.getAllEntries()[0] + const { id, timestamp, ...activity } = activityLog.getAllEntries()[0] assert.deepStrictEqual(activity, { - id: '1', source: 'Station', type: 'info', message: 'Hello world!' }) + assert.equal(typeof id, 'string') assertTimestampIsCloseToNow(timestamp, 'activity.timestamp') }) @@ -35,19 +37,17 @@ describe('ActivityLog', function () { const activityLog = new ActivityLog() activityLog.record(givenActivity({ message: 'one' })) activityLog.record(givenActivity({ message: 'two' })) - assert.deepStrictEqual(activityLog.getAllEntries().map(it => pickProps(it, 'id', 'message')), [ - { id: '1', message: 'one' }, - { id: '2', message: 'two' } - ]) + const [first, second] = activityLog.getAllEntries() + assert(first.id !== second.id, `Expected unique ids. Got the same value: ${first.id}`) }) it('preserves activities across restarts', function () { new ActivityLog().record(givenActivity({ message: 'first run' })) const activityLog = new ActivityLog() activityLog.record(givenActivity({ message: 'second run' })) - assert.deepStrictEqual(activityLog.getAllEntries().map(it => pickProps(it, 'id', 'message')), [ - { id: '1', message: 'first run' }, - { id: '2', message: 'second run' } + assert.deepStrictEqual(activityLog.getAllEntries().map(it => pickProps(it, 'message')), [ + { message: 'first run' }, + { message: 'second run' } ]) })