Skip to content

Commit

Permalink
feat: better json codec and avoid reverse if not needed
Browse files Browse the repository at this point in the history
  • Loading branch information
hugomrdias committed Dec 24, 2023
1 parent 21fb394 commit 849c23f
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 17 deletions.
3 changes: 2 additions & 1 deletion packages/iso-kv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
],
"scripts": {
"lint": "tsc --build && eslint . && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore",
"test": "tsc --build && pnpm run test:node && pnpm run test:browser",
"test": "pnpm run test:node && pnpm run test:browser",
"test:node": "playwright-test 'test/**/!(*.browser).test.js' --mode node",
"test:browser": "playwright-test 'test/**/!(*.node).test.js'"
},
Expand All @@ -60,6 +60,7 @@
"conf": "^12.0.0",
"delay": "^6.0.0",
"playwright-test": "^14.0.0",
"quick-lru": "^7.0.0",
"tempy": "^3.1.0",
"typescript": "5.3.3"
},
Expand Down
8 changes: 7 additions & 1 deletion packages/iso-kv/src/adapters/file.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Conf from 'conf'
import { parse, stringify } from '../json.js'

/**
* @typedef {import('../types').KvStorageAdapter} KvStorageAdapter
Expand All @@ -13,7 +14,12 @@ export class FileStorageAdapter {
* @param {import('conf').Options<Map<string, unknown>>} [config]
*/
constructor(config = {}) {
this.conf = new Conf(config)
this.conf = new Conf({
...config,
serialize: (value) => stringify(value),
deserialize: (value) => parse(value),
accessPropertiesByDotNotation: false,
})
}

/**
Expand Down
6 changes: 4 additions & 2 deletions packages/iso-kv/src/adapters/sql.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Kysely } from 'kysely'
import { parse, stringify } from '../json.js'

/**
* @typedef {import('../types').KvStorageAdapter} KvStorageAdapter
Expand All @@ -9,7 +10,7 @@ import { Kysely } from 'kysely'
* @param {unknown} data
*/
export function serialize(data) {
return JSON.stringify(data)
return stringify(data)
}

/**
Expand All @@ -20,7 +21,7 @@ export function deserialize(data) {
return
}
// @ts-ignore
return JSON.parse(data)
return parse(data)
}

/**
Expand Down Expand Up @@ -151,6 +152,7 @@ export class SqlStorageAdapter {
.orderBy('key')
.selectAll()
.execute()

for (const { key, value } of data) {
yield {
key,
Expand Down
5 changes: 3 additions & 2 deletions packages/iso-kv/src/adapters/web-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
* @typedef {import('../types').KvKey} KvKey
*/

import { parse, stringify } from '../json.js'
import { MemoryStorageAdapter } from './memory.js'

/**
* @param {unknown} data
*/
export function serialize(data) {
return JSON.stringify(data)
return stringify(data)
}

/**
* @param {unknown} data
*/
export function deserialize(data) {
// @ts-ignore
return JSON.parse(data)
return parse(data)
}

/**
Expand Down
21 changes: 14 additions & 7 deletions packages/iso-kv/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ function checkValue(value) {
* @param {KvKey} key
*/
function join(key) {
return key.join(':')
return key
.map((v) => {
return v instanceof Date ? v.toISOString() : v.toString()
})
.join(' ')
}

/**
* @param {string} key
*/
function split(key) {
return key.split(':')
return key.split(' ')
}

/**
Expand Down Expand Up @@ -231,7 +235,11 @@ export class KV {
continue
}

data.push({ key, value })
if (reverse) {
data.push({ key, value })
} else {
yield /** @type {import('./types').KvEntry} */ ({ key, value })
}

count++
if (limit !== undefined && count >= limit) {
Expand All @@ -241,10 +249,9 @@ export class KV {

if (reverse) {
data.reverse()
}

for (const item of data) {
yield /** @type {import('./types').KvEntry} */ (item)
for (const item of data) {
yield /** @type {import('./types').KvEntry} */ (item)
}
}
}
}
57 changes: 57 additions & 0 deletions packages/iso-kv/src/json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// JSON.stringify and JSON.parse with URL, Map and Uint8Array type support.

/**
* Json replacer with URL, Map, Set, BitInt, RegExp and Uint8Array type support.
*
* @param {string} k
* @param {any} v
*/
export const replacer = (k, v) => {
if (v instanceof URL) {
return { $url: v.toString() }
} else if (v instanceof Map) {
return { $map: [...v.entries()] }
} else if (v instanceof Uint8Array) {
return { $bytes: [...v.values()] }
} else if (v instanceof ArrayBuffer) {
return { $bytes: [...new Uint8Array(v).values()] }
} else if (v?.type === 'Buffer' && Array.isArray(v.data)) {
return { $bytes: v.data }
} else if (typeof v === 'bigint') {
return { $bigint: v.toString() }
} else if (v instanceof Set) {
return { $set: [...v.values()] }
} else if (v instanceof RegExp) {
return { $regex: [v.source, v.flags] }
}
return v
}

// const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
/**
* Json reviver with URL, Map, Set, BitInt, RegExp and Uint8Array type support.
*
* @param {string} k - key
* @param {any} v - value
*/
export const reviver = (k, v) => {
if (!v) return v
if (v.$url) return new URL(v.$url)
if (v.$map) return new Map(v.$map)
if (v.$bytes) return new Uint8Array(v.$bytes)
if (v.$bigint) return BigInt(v.$bigint)
// if (typeof v === 'string' && isoDateRegex.test(v)) return new Date(v)
if (v.$set) return new Set(v.$set)
if (v.$regex) return new RegExp(v.$regex[0], v.$regex[1])
return v
}

/**
* @param {any} value
* @param {number|string} [space]
*/
export const stringify = (value, space) =>
JSON.stringify(value, replacer, space)

/** @param {string} value */
export const parse = (value) => JSON.parse(value, reviver)
2 changes: 1 addition & 1 deletion packages/iso-kv/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface KvListOptions {
reverse?: boolean
}

export type KvListIterator<T> = Iterator<T> | AsyncIterator<T>
export type KvListIterator<T> = IterableIterator<T> | AsyncIterableIterator<T>

export interface KvStorageAdapter {
get: <T = unknown>(key: string) => Await<T | undefined>
Expand Down
61 changes: 60 additions & 1 deletion packages/iso-kv/test/base.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-null */
import { assert } from 'playwright-test/taps'
import delay from 'delay'

Expand All @@ -6,7 +7,12 @@ import delay from 'delay'
* @param {import('playwright-test/taps').Suite} suite
*/
export function baseTests(kv, suite) {
const { test } = suite
const { test, beforeEach } = suite

beforeEach(async () => {
await kv.clear()
})

test('should set/get', async () => {
await kv.set(['foo'], 1)

Expand Down Expand Up @@ -158,4 +164,57 @@ export function baseTests(kv, suite) {

assert.equal(value, 1)
})

test('should support data types', async () => {
let v
await kv.set(['undefined'], undefined)
assert.equal(undefined, await kv.get(['undefined']))

await kv.set(['null'], null)
assert.equal(null, await kv.get(['null']))

await kv.set(['string'], 'string')
assert.equal('string', await kv.get(['string']))

await kv.set(['number'], 1)
assert.equal(1, await kv.get(['number']))

await kv.set(['bigint'], BigInt(1))
assert.equal(BigInt(1), await kv.get(['bigint']))

v = /^[\s\w!,?àáâãçèéêíïñóôõöú-]+$/
await kv.set(['regex'], v)
assert.deepEqual(v, await kv.get(['regex']))

v = new Set(['a', 1, new Set(['c', 'd'])])
await kv.set(['set'], v)
assert.deepEqual(v, await kv.get(['set']))
})

test('should order', async () => {
await kv.set([new Date('2000-01-01T11:00:00.000Z')], 2)
await kv.set([new Date('2000-01-01T10:00:00.000Z')], 3)
await kv.set([new Date('2000-01-01T09:00:00.000Z')], 1)
await kv.set([2], 4)
await kv.set([1], 1)
// await kv.set(['users', Date.now(), crypto.randomUUID()], 1)
// await kv.set(['users', Date.now(), crypto.randomUUID()], 1)
// await kv.set(['users', Date.now(), crypto.randomUUID()], 1)

const expected = [
['1'],
['2'],
['2000-01-01T09:00:00.000Z'],
['2000-01-01T10:00:00.000Z'],
['2000-01-01T11:00:00.000Z'],
]

const actual = []

for await (const { key } of kv) {
actual.push(key)
}

assert.deepEqual(actual, expected)
})
}
2 changes: 0 additions & 2 deletions packages/iso-kv/test/base.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,3 @@ const kv = new KV({
})

baseTests(kv, suite('Memory'))

// const test = suite('Misc')
23 changes: 23 additions & 0 deletions packages/iso-kv/test/json.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { assert, suite } from 'playwright-test/taps'
import { parse, stringify } from '../src/json.js'

const { test } = suite('json')

test('should support data types', async () => {
let v
assert.deepEqual(parse(stringify({ a: 1 })), { a: 1 })
assert.deepEqual(parse(stringify([1n])), [1n])
assert.deepEqual(parse(stringify({ a: { b: 1n } })), { a: { b: 1n } })

v = new Map([['a', { b: 1n }]])
assert.deepEqual(parse(stringify(v)), v)

// v = new Date()
// assert.deepEqual(parse(stringify(v)), v)

v = new Set(['a', 1, new Set(['c', 'd'])])
assert.deepEqual(parse(stringify(v)), v)

v = /^[\s\w!,?àáâãçèéêíïñóôõöú-]+$/
assert.deepEqual(parse(stringify(v)), v)
})

0 comments on commit 849c23f

Please sign in to comment.