@idooel/runtime-context
Version:
Runtime data pool with namespaces, stackable contexts, subscriptions and optional persistence. Vue adapter included.
354 lines (278 loc) • 10.7 kB
text/typescript
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createDataPool } from '../../src/core/DataPool'
import type { DataEvent } from '../../src/core/types'
describe('DataPool - Core Functionality', () => {
let pool: ReturnType<typeof createDataPool>
beforeEach(() => {
pool = createDataPool()
})
describe('Basic Operations', () => {
it('should set and get values', () => {
pool.set('user', 'name', 'John')
expect(pool.get('user', 'name')).toBe('John')
})
it('should return fallback value for non-existent keys', () => {
expect(pool.get('user', 'name', 'default')).toBe('default')
expect(pool.get('user', 'nonexistent')).toBeUndefined()
})
it('should handle different data types', () => {
const testData = {
string: 'test',
number: 42,
boolean: true,
object: { key: 'value' },
array: [1, 2, 3],
null: null,
undefined: undefined
}
Object.entries(testData).forEach(([key, value]) => {
pool.set('test', key, value)
expect(pool.get('test', key)).toBe(value)
})
})
it('should update existing values', () => {
pool.set('user', 'name', 'John')
pool.set('user', 'name', 'Jane')
expect(pool.get('user', 'name')).toBe('Jane')
})
it('should handle multiple namespaces', () => {
pool.set('user', 'name', 'John')
pool.set('app', 'theme', 'dark')
pool.set('settings', 'language', 'en')
expect(pool.get('user', 'name')).toBe('John')
expect(pool.get('app', 'theme')).toBe('dark')
expect(pool.get('settings', 'language')).toBe('en')
})
})
describe('Namespace Prefix', () => {
it('should automatically add idooel prefix to namespaces', () => {
pool.set('user', 'token', 'abc123')
// Check internal storage by accessing snapshot
const snapshot = pool.snapshot({ merged: true })
expect(snapshot).toHaveProperty('idooel.user')
expect(snapshot['idooel.user']).toHaveProperty('token', 'abc123')
})
it('should not double-prefix existing idooel namespaces', () => {
pool.set('idooel.user', 'token', 'xyz789')
const snapshot = pool.snapshot({ merged: true })
expect(snapshot).toHaveProperty('idooel.user')
expect(snapshot['idooel.user'].token).toBe('xyz789')
})
it('should access values correctly regardless of prefix in user code', () => {
pool.set('user', 'data', 'value')
pool.set('idooel.user', 'prefixed', 'data')
// Both should work from user perspective
expect(pool.get('user', 'data')).toBe('value')
expect(pool.get('user', 'prefixed')).toBe('data')
})
})
describe('Delete Operations', () => {
it('should delete specific keys', () => {
pool.set('user', 'name', 'John')
pool.set('user', 'age', 30)
pool.delete('user', 'name')
expect(pool.get('user', 'name')).toBeUndefined()
expect(pool.get('user', 'age')).toBe(30)
})
it('should handle deleting non-existent keys', () => {
expect(() => pool.delete('user', 'nonexistent')).not.toThrow()
})
it('should handle deleting from non-existent namespaces', () => {
expect(() => pool.delete('nonexistent', 'key')).not.toThrow()
})
})
describe('Clear Operations', () => {
it('should clear specific namespace', () => {
pool.set('user', 'name', 'John')
pool.set('user', 'age', 30)
pool.set('app', 'theme', 'dark')
pool.clear('user')
expect(pool.get('user', 'name')).toBeUndefined()
expect(pool.get('user', 'age')).toBeUndefined()
expect(pool.get('app', 'theme')).toBe('dark')
})
it('should clear all namespaces', () => {
pool.set('user', 'name', 'John')
pool.set('app', 'theme', 'dark')
pool.set('settings', 'language', 'en')
pool.clear()
expect(pool.get('user', 'name')).toBeUndefined()
expect(pool.get('app', 'theme')).toBeUndefined()
expect(pool.get('settings', 'language')).toBeUndefined()
})
})
describe('Subscription System', () => {
it('should notify subscribers of data changes', () => {
const callback = vi.fn()
pool.subscribe({ ns: 'user', key: 'name' }, callback)
pool.set('user', 'name', 'John')
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
ns: 'user', // Should be original namespace, not prefixed
key: 'name',
value: 'John',
source: 'global'
})
)
})
it('should notify subscribers of data deletion', () => {
pool.set('user', 'name', 'John')
const callback = vi.fn()
pool.subscribe({ ns: 'user', key: 'name' }, callback)
pool.delete('user', 'name')
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
ns: 'user',
key: 'name',
value: undefined,
source: 'delete'
})
)
})
it('should notify subscribers of namespace clearing', () => {
pool.set('user', 'name', 'John')
pool.set('user', 'age', 30)
const callback = vi.fn()
// Subscribe to all changes in user namespace to catch clear events
pool.subscribe({ ns: 'user' }, callback)
pool.clear('user')
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
ns: 'user',
key: '*',
value: undefined,
source: 'clear'
})
)
})
it('should support wildcard subscriptions', () => {
const callback = vi.fn()
pool.subscribe({}, callback) // Subscribe to all changes
pool.set('user', 'name', 'John')
pool.set('app', 'theme', 'dark')
pool.delete('user', 'name')
expect(callback).toHaveBeenCalledTimes(3)
})
it('should support namespace-only subscriptions', () => {
const callback = vi.fn()
pool.subscribe({ ns: 'user' }, callback)
pool.set('user', 'name', 'John')
pool.set('user', 'age', 30)
pool.set('app', 'theme', 'dark') // Should not trigger
expect(callback).toHaveBeenCalledTimes(2)
})
it('should handle unsubscribe correctly', () => {
const callback = vi.fn()
const unsubscribe = pool.subscribe({ ns: 'user', key: 'name' }, callback)
unsubscribe()
pool.set('user', 'name', 'John')
expect(callback).not.toHaveBeenCalled()
})
it('should handle subscriber errors gracefully', () => {
const errorCallback = vi.fn(() => {
throw new Error('Subscriber error')
})
const normalCallback = vi.fn()
pool.subscribe({ ns: 'user', key: 'name' }, errorCallback)
pool.subscribe({ ns: 'user', key: 'name' }, normalCallback)
expect(() => pool.set('user', 'name', 'John')).not.toThrow()
expect(normalCallback).toHaveBeenCalled()
})
})
describe('Context System', () => {
it('should create context with initial data', () => {
const context = pool.createContext({
user: { name: 'John', role: 'admin' },
app: { theme: 'dark' }
})
expect(context).toBeDefined()
expect(context.id).toBeDefined()
})
it('should run operations in context', () => {
pool.set('global', 'value', 'global')
const context = pool.createContext({
global: { value: 'context' }
})
pool.runInContext(context, () => {
expect(pool.get('global', 'value')).toBe('context')
})
expect(pool.get('global', 'value')).toBe('global')
})
it('should support nested contexts', () => {
const outerContext = pool.createContext({
user: { name: 'John' }
})
const innerContext = pool.createContext({
user: { age: 30 }
})
pool.runInContext(outerContext, () => {
expect(pool.get('user', 'name')).toBe('John')
expect(pool.get('user', 'age')).toBeUndefined()
pool.runInContext(innerContext, () => {
expect(pool.get('user', 'name')).toBe('John')
expect(pool.get('user', 'age')).toBe(30)
})
expect(pool.get('user', 'age')).toBeUndefined()
})
})
it('should handle context-specific data setting', () => {
const context = pool.createContext()
pool.runInContext(context, () => {
pool.set('context', 'data', 'context-value')
})
expect(pool.get('context', 'data')).toBeUndefined() // Not in global
})
it('should support withContext helper', () => {
const context = pool.createContext({
user: { name: 'Jane' }
})
const contextFn = pool.withContext(context)
const result = contextFn(() => pool.get('user', 'name'))()
expect(result).toBe('Jane')
})
it('should wrap handlers with current context', () => {
const context = pool.createContext({
user: { name: 'ContextUser' }
})
pool.runInContext(context, () => {
const handler = pool.wrapHandler(() => pool.get('user', 'name'))
const result = handler()
expect(result).toBe('ContextUser')
})
})
})
describe('Snapshot Functionality', () => {
it('should create merged snapshot', () => {
pool.set('user', 'name', 'John')
pool.set('app', 'theme', 'dark')
const snapshot = pool.snapshot({ merged: true })
expect(snapshot).toEqual({
'idooel.user': { name: 'John' },
'idooel.app': { theme: 'dark' }
})
})
it('should create detailed snapshot', () => {
pool.set('user', 'name', 'John')
const context = pool.createContext({ user: { role: 'admin' } })
// Simulate context being active
const token = pool.enter(context)
pool.set('user', 'age', 30)
// Get snapshot while context is still active
const snapshot = pool.snapshot({ merged: false })
pool.exit(token)
expect(snapshot.global).toEqual({
'idooel.user': { name: 'John' }
})
expect(snapshot.contexts).toHaveLength(1)
expect(snapshot.contexts[0].data).toEqual({
'idooel.user': { role: 'admin', age: 30 }
})
})
it('should handle empty pool snapshot', () => {
const snapshot = pool.snapshot({ merged: true })
expect(snapshot).toEqual({})
})
})
})