diff --git a/src/backend/common/Cache.ts b/src/backend/common/Cache.ts index b0ed73a6..a4d7a815 100644 --- a/src/backend/common/Cache.ts +++ b/src/backend/common/Cache.ts @@ -19,7 +19,6 @@ import { asCacheConfig, CacheAuthProvider, CacheConfig, CacheConfigOptions, Cach import { Typeson } from 'typeson'; import { builtin } from 'typeson-registry'; import { loggerNoop } from './MaybeLogger.js'; -import { ListenProgressPositional, ListenProgressTS } from '../sources/PlayerState/ListenProgress.js'; const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); import prom, { Gauge } from 'prom-client'; import { nonEmptyStringOrDefault } from '../../core/StringUtils.js'; @@ -39,9 +38,7 @@ typeson.register({ (x) => dayjs.isDayjs(x), (d: Dayjs) => d.toJSON(), (date) => dayjs(date) - ], - ListenProgressTS, - ListenProgressPositional + ] }); const unsupportedEnvKeys = [ diff --git a/src/backend/tests/cache/cache.test.ts b/src/backend/tests/cache/cache.test.ts index ecc59ca9..c21f2f68 100644 --- a/src/backend/tests/cache/cache.test.ts +++ b/src/backend/tests/cache/cache.test.ts @@ -4,15 +4,10 @@ import asPromised from 'chai-as-promised'; import { after, before, describe, it } from 'mocha'; import dayjs from "dayjs"; import withLocalTmpDir from 'with-local-tmp-dir'; -import { initFileCache, initMemoryCache, initValkeyCache, MSCache } from "../../common/Cache.js"; -import { generatePlays } from "../../../core/PlayTestUtils.js"; +import { initFileCache, initMemoryCache, initValkeyCache } from "../../common/Cache.js"; import { ListenProgressPositional, ListenProgressTS } from "../../sources/PlayerState/ListenProgress.js"; import { isPortReachableConnect } from "../../utils/NetworkUtils.js"; -import { getRoot } from "../../ioc.js"; -import { transientCache } from "../utils/TransientTestUtils.js"; -import { TestScrobbler } from "../scrobbler/TestScrobbler.js"; import { sleep } from "../../utils.js"; -import {promises} from 'node:fs'; chai.use(asPromised); @@ -56,16 +51,24 @@ describe('#Caching', function () { it('File cache serializes and deserializes dayjs', async function () { - withLocalTmpDir(async () => { + await withLocalTmpDir(async () => { + // for some reason this *recreates* an empty cache after the test has finished (when running the full suite) + // i think its because of long persist interval compared to test time? + // + // so: + // make intervals very small + // call destroy on both cache instances (shouldn't be necessary) + // sleep for longer than persist interal so tmp dir callback can (hopefully) properly delete any files - const [keyv, flat] = await initFileCache({ cacheDir: process.cwd() }); + const [keyv, flat] = await initFileCache({ cacheDir: process.cwd(), persistInterval: 5, expirationInterval: 4 }); const now = dayjs(); await keyv.set('foo', now); - flat.save(); + flat.save(true); + keyv.disconnect(); - const [cleanKeyv, cleanFlat] = await initFileCache({ cacheDir: process.cwd() }); + const [cleanKeyv, cleanFlat] = await initFileCache({ cacheDir: process.cwd(), persistInterval: 5, expirationInterval: 4 }); const time = await cleanKeyv.get('foo'); @@ -73,31 +76,9 @@ describe('#Caching', function () { expect(time instanceof dayjs).is.true; expect(now.toJSON()).eq((time as any).toJSON()); flat.destroy(); - - }, { unsafeCleanup: true }); - }); - - it('File cache serializes and deserializes ListenProgress', async function () { - - withLocalTmpDir(async () => { - - const [keyv, flat] = await initFileCache({ cacheDir: process.cwd() }); - - const prog = new ListenProgressPositional({ timestamp: dayjs(), position: 35, positionPercent: 50 }); - - await keyv.set('foo', prog); - await flat.save(); - - const [cleanKeyv, cleanFlat] = await initFileCache({ cacheDir: process.cwd() }); - - const cachedProg = await cleanKeyv.get('foo'); - - expect(cachedProg).to.not.be.undefined; - expect(cachedProg instanceof ListenProgressTS).is.true; - expect(cachedProg.timestamp.toJSON()).eq(prog.timestamp.toJSON()); - flat.destroy(); - - }, { unsafeCleanup: true }); + cleanFlat.destroy(); + await sleep(10); + }, { unsafeCleanup: true, postfix: 'fileCacheDajys' }); }); }); @@ -127,62 +108,5 @@ describe('#Caching', function () { expect(now.toJSON()).eq((time as any).toJSON()); }); - - it('Valkey cache serializes and deserializes ListenProgress', async function () { - - const keyv = await initValkeyCache('test', 'redis://valkey:6379'); - await keyv.clear(); - - const prog = new ListenProgressPositional({ timestamp: dayjs(), position: 35, positionPercent: 50 }); - - await keyv.set('foo', prog); - - const cachedProg = await keyv.get('foo'); - - expect(cachedProg).to.not.be.undefined; - expect(cachedProg instanceof ListenProgressTS).is.true; - expect(cachedProg.timestamp.toJSON()).eq(prog.timestamp.toJSON()); - - }); }); - - // describe('#ScrobbleCache', function () { - - // afterEach(function () { - // const root = getRoot(); - // root.upsert({ cache: () => transientCache }); - // root.items.cache().init(); - // }); - - // it('Preserves scrobbles', async function () { - - // this.timeout(10000); - - // // why does this take so long? - // await withLocalTmpDir(async () => { - - // const root = getRoot(); - // root.upsert({ cache: () => () => new MSCache(loggerTest, { scrobble: { provider: 'file', connection: process.cwd(), persistInterval: 100 } }) }); - - // await using test = new TestScrobbler(); - // await test.initialize(); - // const plays = generatePlays(100, {}, {}, {listenRanges: true}); - // await test.queueScrobble(plays, 'testSource'); - // const queued = test.queuedScrobbles.map(x => x.play); - // await sleep(101); - // const dirContents = await promises.readdir('.'); - // const hasCache = dirContents.some(x => x === 'ms-scrobble.cache'); - // expect(hasCache).is.true; - - // await using newTest = new TestScrobbler(); - // await newTest.initialize(); - // expect(newTest.queuedScrobbles.length).to.eq(plays.length); - // expect(newTest.queuedScrobbles[0].play.data.track).to.eq(queued[0].data.track); - - // }, { unsafeCleanup: true }); - - // }); - - // }); - }); diff --git a/src/backend/tests/config/config.test.ts b/src/backend/tests/config/config.test.ts index 10717d91..7482074a 100644 --- a/src/backend/tests/config/config.test.ts +++ b/src/backend/tests/config/config.test.ts @@ -44,7 +44,7 @@ describe('Sample Configs', function () { let reset: any; beforeEach(async function() { - reset = await withLocalTmpDir({unsafeCleanup: true}); + reset = await withLocalTmpDir({unsafeCleanup: true, postfix: 'sourceConfigParse'}); }); afterEach(async function() { @@ -78,7 +78,7 @@ describe('Sample Configs', function () { let reset: any; beforeEach(async function() { - reset = await withLocalTmpDir({unsafeCleanup: true}); + reset = await withLocalTmpDir({unsafeCleanup: true, postfix: 'clientConfigParse'}); }); afterEach(async function() { diff --git a/src/backend/tests/database/drizzle.test.ts b/src/backend/tests/database/drizzle.test.ts index 9faae8cb..fcac0dfc 100644 --- a/src/backend/tests/database/drizzle.test.ts +++ b/src/backend/tests/database/drizzle.test.ts @@ -31,7 +31,7 @@ describe('Migrations', function () { const [shouldBackup, pending] = await shouldBackupDb(getDbPath('notreal', process.cwd())); expect(shouldBackup).is.false; expect(pending).length(0); - }); + }, {postfix: 'noDb'}); }); @@ -43,7 +43,7 @@ describe('Migrations', function () { expect(shouldBackup).is.true; expect(pending).length(0); otherdb.close(); - }, { unsafeCleanup: true }); + }, { unsafeCleanup: true, postfix: 'badDb' }); }); @@ -82,7 +82,7 @@ describe('Migrations', function () { } catch (e) { throw e; } - }, { unsafeCleanup: true }); + }, { unsafeCleanup: true, postfix: 'pendingMigrations' }); }); it('Detects no pending migrations correctly', async function () { @@ -107,7 +107,7 @@ describe('Migrations', function () { } catch (e) { throw e; } - }, { unsafeCleanup: true }); + }, { unsafeCleanup: true, postfix: 'noMigrations' }); }); it('Backs up database when migrations are pending', async function () { @@ -149,7 +149,7 @@ describe('Migrations', function () { } catch (e) { throw e; } - }, { unsafeCleanup: true }); + }, { unsafeCleanup: true, postfix: 'dbBackup' }); }); }); @@ -572,7 +572,7 @@ describe('DB Size Stats', function () { } catch (e) { throw e; } - }, { unsafeCleanup: true }); + }, { unsafeCleanup: true, postfix: 'dbStatEmpty' }); }); it('get db plays size stats', async function () { @@ -597,7 +597,7 @@ describe('DB Size Stats', function () { } catch (e) { throw e; } - }, { unsafeCleanup: true }); + }, { unsafeCleanup: true, postfix: 'dbStatPlain' }); }); it('get db plays size stats with input', async function () { @@ -622,7 +622,7 @@ describe('DB Size Stats', function () { } catch (e) { throw e; } - }, { unsafeCleanup: true }); + }, { unsafeCleanup: true, postfix: 'dbStatInput' }); }); it('get db plays size stats with input and lifecycle', async function () { @@ -647,6 +647,6 @@ describe('DB Size Stats', function () { } catch (e) { throw e; } - }, { unsafeCleanup: true }); + }, { unsafeCleanup: true, postfix: 'dbStatAll' }); }); }) \ No newline at end of file diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index ca13f102..ca129b02 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -605,6 +605,9 @@ export interface numberFormatOptions { * It needs to be cheap since we mostly use this when walking play objects to transform strings back to dayjs and there may be many strings to check */ export const REGEX_ISO8601_LOOSE = new RegExp(/\d{4}-[01]\d-[0-3]\dT/); +/** A string we previously marshalled has a wellknown prefix and only check for DateT since we can reasonbly sure if this exists its a date we can parse with dayjs + */ +export const REGEX_ISO8601_WELLKNOWN = new RegExp(/dayjs-(\d{4}-[01]\d-[0-3]\dT.*)/); export const CLIENT_INGRESS_QUEUE = 'ingress'; export const CLIENT_DEAD_QUEUE = 'dead'; diff --git a/src/core/PlayMarshalUtils.ts b/src/core/PlayMarshalUtils.ts index eeb60a0d..e7f5a3fe 100644 --- a/src/core/PlayMarshalUtils.ts +++ b/src/core/PlayMarshalUtils.ts @@ -101,3 +101,14 @@ export const asPlayCheap = (data: JsonPlayObject | PlayObject): PlayObject => { return data as unknown as PlayObject; }; +export const marshalDayjsToWellKnownString = (ctx: TraverseContext, x: any): void => { + if (dayjs.isDayjs(x)) { + ctx.update(`dayjs-x.toISOString()`); + } +} + +export const marshalWellKnownIsoStringToDayjs = (ctx: TraverseContext, x: any): void => { + if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) { + ctx.update(dayjs(x.substring(6)), true); + } +} \ No newline at end of file