diff --git a/DeltaBased/CausalContext/__tests__/dot-context.test.js b/DeltaBased/CausalContext/__tests__/dot-context.test.js new file mode 100644 index 0000000..4ea6c61 --- /dev/null +++ b/DeltaBased/CausalContext/__tests__/dot-context.test.js @@ -0,0 +1,410 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { DotContext } = require('../dot-context.js'); +const { Dot } = require('../dot.js'); + +// Helper function to create test context +function createContext(cc, dc) { + const ctx = new DotContext(); + for (const [id, counter] of Object.entries(cc)) { + ctx.compactCausalContext.set(id, counter); + } + for (const dotStr of dc) { + ctx.dotCloud.add(dotStr); + } + return ctx; +} + +describe('DotContext', () => { + describe('dotIn()', () => { + it('should return true for dot in Compact Causal Context', () => { + const ctx = createContext({ a: 5 }, []); + assert.equal(ctx.dotIn({ id: 'a', counter: 3 }), true); + }); + + it('should return true for dot at CC boundary', () => { + const ctx = createContext({ a: 5 }, []); + assert.equal(ctx.dotIn({ id: 'a', counter: 5 }), true); + }); + + it('should return true for dot in Dot Cloud', () => { + const ctx = createContext({ a: 5 }, ['a:7']); + assert.equal(ctx.dotIn({ id: 'a', counter: 7 }), true); + }); + + it('should return false for unknown dot (gap)', () => { + const ctx = createContext({ a: 5 }, ['a:7']); + assert.equal(ctx.dotIn({ id: 'a', counter: 6 }), false); + }); + + it('should return false for unknown replica', () => { + const ctx = createContext({ a: 5 }, []); + assert.equal(ctx.dotIn({ id: 'b', counter: 1 }), false); + }); + + it('should accept dot as string', () => { + const ctx = createContext({ a: 5 }, []); + assert.equal(ctx.dotIn('a:3'), true); + }); + + it('should accept dot as Dot instance', () => { + const ctx = createContext({ a: 5 }, []); + const dot = new Dot('a', 3); + assert.equal(ctx.dotIn(dot), true); + }); + + it('should return false for counter beyond CC', () => { + const ctx = createContext({ a: 5 }, []); + assert.equal(ctx.dotIn({ id: 'a', counter: 6 }), false); + }); + }); + + describe('makeDot()', () => { + it('should create first dot (id, 1) for new replica', () => { + const ctx = new DotContext(); + const dot = ctx.makeDot('a'); + assert.equal(dot.id, 'a'); + assert.equal(dot.counter, 1); + assert.equal(ctx.compactCausalContext.get('a'), 1); + }); + + it('should increment counter for existing replica', () => { + const ctx = createContext({ a: 3 }, []); + const dot = ctx.makeDot('a'); + assert.equal(dot.id, 'a'); + assert.equal(dot.counter, 4); + assert.equal(ctx.compactCausalContext.get('a'), 4); + }); + + it('should produce sequential counters for multiple calls', () => { + const ctx = new DotContext(); + const dot1 = ctx.makeDot('a'); + const dot2 = ctx.makeDot('a'); + const dot3 = ctx.makeDot('a'); + assert.equal(dot1.counter, 1); + assert.equal(dot2.counter, 2); + assert.equal(dot3.counter, 3); + }); + + it('should maintain independent counters per replica', () => { + const ctx = new DotContext(); + const dotA1 = ctx.makeDot('a'); + const dotB1 = ctx.makeDot('b'); + const dotA2 = ctx.makeDot('a'); + assert.equal(dotA1.counter, 1); + assert.equal(dotB1.counter, 1); + assert.equal(dotA2.counter, 2); + }); + }); + + describe('insertDot()', () => { + it('should add dot to Dot Cloud', () => { + const ctx = new DotContext(); + ctx.insertDot({ id: 'a', counter: 5 }, false); + assert.ok(ctx.dotCloud.has('a:5')); + }); + + it('should trigger compaction by default', () => { + const ctx = createContext({ a: 3 }, []); + ctx.insertDot({ id: 'a', counter: 4 }); + assert.equal(ctx.dotCloud.size, 0); + assert.equal(ctx.compactCausalContext.get('a'), 4); + }); + + it('should skip compaction when compactNow=false', () => { + const ctx = createContext({ a: 3 }, []); + ctx.insertDot({ id: 'a', counter: 4 }, false); + assert.ok(ctx.dotCloud.has('a:4')); + assert.equal(ctx.compactCausalContext.get('a'), 3); + }); + + it('should accept dot as string', () => { + const ctx = new DotContext(); + ctx.insertDot('a:5', false); + assert.ok(ctx.dotCloud.has('a:5')); + }); + + it('should accept dot as Dot instance', () => { + const ctx = new DotContext(); + const dot = new Dot('a', 5); + ctx.insertDot(dot, false); + assert.ok(ctx.dotCloud.has('a:5')); + }); + }); + + describe('compact()', () => { + it('should compact single contiguous dot at counter=1', () => { + const ctx = createContext({}, ['a:1']); + ctx.compact(); + assert.equal(ctx.dotCloud.size, 0); + assert.equal(ctx.compactCausalContext.get('a'), 1); + }); + + it('should compact contiguous sequence', () => { + const ctx = createContext({ a: 3 }, ['a:4']); + ctx.compact(); + assert.equal(ctx.dotCloud.size, 0); + assert.equal(ctx.compactCausalContext.get('a'), 4); + }); + + it('should remove dominated dots', () => { + const ctx = createContext({ a: 5 }, ['a:3']); + ctx.compact(); + assert.equal(ctx.dotCloud.size, 0); + assert.equal(ctx.compactCausalContext.get('a'), 5); + }); + + it('should keep non-contiguous dots', () => { + const ctx = createContext({ a: 3 }, ['a:5']); + ctx.compact(); + assert.ok(ctx.dotCloud.has('a:5')); + assert.equal(ctx.compactCausalContext.get('a'), 3); + }); + + it('should compact multiple times for best order', () => { + const ctx = createContext({}, []); + ctx.insertDot({ id: 'a', counter: 2 }, false); + ctx.insertDot({ id: 'a', counter: 1 }, false); + ctx.compact(); + assert.equal(ctx.dotCloud.size, 0); + assert.equal(ctx.compactCausalContext.get('a'), 2); + }); + + it('should compact multiple replicas independently', () => { + const ctx = createContext({ c: 1 }, ['a:1', 'b:1', 'c:2']); + ctx.compact(); + assert.equal(ctx.dotCloud.size, 0); + assert.equal(ctx.compactCausalContext.get('a'), 1); + assert.equal(ctx.compactCausalContext.get('b'), 1); + assert.equal(ctx.compactCausalContext.get('c'), 2); + }); + + it('should handle complex compaction scenario', () => { + const ctx = createContext({ a: 1 }, ['a:2', 'a:3', 'b:1']); + ctx.compact(); + assert.equal(ctx.dotCloud.size, 0); + assert.equal(ctx.compactCausalContext.get('a'), 3); + assert.equal(ctx.compactCausalContext.get('b'), 1); + }); + + it('should compact sequential dots', () => { + const ctx = createContext({}, ['a:1', 'a:2', 'a:3']); + ctx.compact(); + assert.equal(ctx.dotCloud.size, 0); + assert.equal(ctx.compactCausalContext.get('a'), 3); + }); + + it('should not compact dots with gaps', () => { + const ctx = createContext({}, ['a:1', 'a:3']); + ctx.compact(); + assert.equal(ctx.dotCloud.size, 1); + assert.ok(ctx.dotCloud.has('a:3')); + assert.equal(ctx.compactCausalContext.get('a'), 1); + }); + }); + + describe('join()', () => { + it('should join empty contexts', () => { + const ctx1 = new DotContext(); + const ctx2 = new DotContext(); + ctx1.join(ctx2); + assert.equal(ctx1.compactCausalContext.size, 0); + assert.equal(ctx1.dotCloud.size, 0); + }); + + it('should join Compact Causal Contexts with max', () => { + const ctx1 = createContext({ a: 3 }, []); + const ctx2 = createContext({ a: 5 }, []); + ctx1.join(ctx2); + assert.equal(ctx1.compactCausalContext.get('a'), 5); + }); + + it('should merge disjoint CC entries', () => { + const ctx1 = createContext({ a: 3 }, []); + const ctx2 = createContext({ b: 4 }, []); + ctx1.join(ctx2); + assert.equal(ctx1.compactCausalContext.get('a'), 3); + assert.equal(ctx1.compactCausalContext.get('b'), 4); + }); + + it('should union Dot Clouds', () => { + const ctx1 = createContext({}, ['a:5']); + const ctx2 = createContext({}, ['b:7']); + ctx1.join(ctx2); + assert.ok(ctx1.dotCloud.has('a:5')); + assert.ok(ctx1.dotCloud.has('b:7')); + }); + + it('should compact after join', () => { + const ctx1 = createContext({ a: 3 }, []); + const ctx2 = createContext({}, ['a:4']); + ctx1.join(ctx2); + assert.equal(ctx1.compactCausalContext.get('a'), 4); + assert.equal(ctx1.dotCloud.size, 0); + }); + + it('should be idempotent', () => { + const ctx1 = createContext({ a: 5 }, ['b:3']); + const ctx2 = createContext({ a: 5 }, ['b:3']); + const before = ctx1.clone(); + ctx1.join(ctx2); + ctx1.join(ctx2); + assert.deepEqual( + Array.from(ctx1.compactCausalContext), + Array.from(before.compactCausalContext) + ); + }); + + it('should be commutative', () => { + const ctx1 = createContext({ a: 3 }, []); + const ctx2 = createContext({ a: 5 }, []); + const ctxA = ctx1.clone(); + const ctxB = ctx2.clone(); + ctxA.join(ctxB); + ctxB.join(ctx1); + assert.equal(ctxA.compactCausalContext.get('a'), 5); + assert.equal(ctxB.compactCausalContext.get('a'), 5); + }); + + it('should be associative', () => { + const ctx1 = createContext({ a: 2 }, []); + const ctx2 = createContext({ a: 5 }, ['b:1']); + const ctx3 = createContext({ b: 3 }, ['c:1']); + + const result1 = ctx1.clone(); + result1.join(ctx2); + result1.join(ctx3); + + const temp = ctx2.clone(); + temp.join(ctx3); + const result2 = ctx1.clone(); + result2.join(temp); + + assert.equal( + result1.compactCausalContext.get('a'), + result2.compactCausalContext.get('a') + ); + assert.equal( + result1.compactCausalContext.get('b'), + result2.compactCausalContext.get('b') + ); + }); + + it('should handle self-join gracefully', () => { + const ctx = createContext({ a: 5 }, ['b:3']); + const before = ctx.clone(); + ctx.join(ctx); + assert.deepEqual( + Array.from(ctx.compactCausalContext), + Array.from(before.compactCausalContext) + ); + }); + + it('should join with gaps and compact correctly', () => { + const ctx1 = createContext({ a: 2 }, ['a:5']); + const ctx2 = createContext({ a: 3 }, ['a:4']); + ctx1.join(ctx2); + assert.equal(ctx1.compactCausalContext.get('a'), 5); + assert.equal(ctx1.dotCloud.size, 0); + }); + }); + + describe('toString()', () => { + it('should show empty context', () => { + const ctx = new DotContext(); + const str = ctx.toString(); + assert.equal(str, 'Context: CC () DC ()'); + }); + + it('should show CC entries', () => { + const ctx = createContext({ a: 5, b: 3 }, []); + const str = ctx.toString(); + assert.ok(str.includes('a:5')); + assert.ok(str.includes('b:3')); + }); + + it('should show DC entries', () => { + const ctx = createContext({}, ['a:7', 'c:2']); + const str = ctx.toString(); + assert.ok(str.includes('a:7')); + assert.ok(str.includes('c:2')); + }); + + it('should show both CC and DC', () => { + const ctx = createContext({ a: 3 }, ['a:7']); + const str = ctx.toString(); + assert.ok(str.includes('a:3')); + assert.ok(str.includes('a:7')); + }); + }); + + describe('clone()', () => { + it('should create independent copy', () => { + const ctx = createContext({ a: 5 }, ['b:3']); + const copy = ctx.clone(); + + copy.compactCausalContext.set('a', 10); + copy.dotCloud.add('c:1'); + + assert.equal(ctx.compactCausalContext.get('a'), 5); + assert.equal(copy.compactCausalContext.get('a'), 10); + assert.ok(!ctx.dotCloud.has('c:1')); + assert.ok(copy.dotCloud.has('c:1')); + }); + + it('should clone empty context', () => { + const ctx = new DotContext(); + const copy = ctx.clone(); + assert.equal(copy.compactCausalContext.size, 0); + assert.equal(copy.dotCloud.size, 0); + }); + + it('should clone complex context', () => { + const ctx = createContext({ a: 5, b: 3, c: 10 }, ['d:1', 'e:7', 'f:3']); + const copy = ctx.clone(); + assert.equal(copy.compactCausalContext.size, 3); + assert.equal(copy.dotCloud.size, 3); + assert.equal(copy.compactCausalContext.get('a'), 5); + assert.ok(copy.dotCloud.has('d:1')); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty Dot Cloud', () => { + const ctx = new DotContext(); + ctx.compact(); + assert.equal(ctx.dotCloud.size, 0); + }); + + it('should handle empty Compact Causal Context', () => { + const ctx = createContext({}, ['a:5']); + assert.equal(ctx.dotIn({ id: 'a', counter: 5 }), true); + assert.equal(ctx.dotIn({ id: 'a', counter: 1 }), false); + }); + + it('should handle large counter values', () => { + const largeCounter = 1000000; + const ctx = createContext({ a: largeCounter }, []); + assert.equal(ctx.dotIn({ id: 'a', counter: largeCounter }), true); + }); + + it('should handle many replicas', () => { + const ctx = new DotContext(); + for (let i = 0; i < 100; i++) { + ctx.makeDot(`replica${i}`); + } + assert.equal(ctx.compactCausalContext.size, 100); + }); + + it('should handle rapid sequential operations', () => { + const ctx = new DotContext(); + for (let i = 0; i < 1000; i++) { + ctx.makeDot('a'); + } + assert.equal(ctx.compactCausalContext.get('a'), 1000); + }); + }); +}); diff --git a/DeltaBased/CausalContext/__tests__/dot-kernel.test.js b/DeltaBased/CausalContext/__tests__/dot-kernel.test.js new file mode 100644 index 0000000..ba734a7 --- /dev/null +++ b/DeltaBased/CausalContext/__tests__/dot-kernel.test.js @@ -0,0 +1,466 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { DotKernel } = require('../dot-kernel.js'); +const { DotContext } = require('../dot-context.js'); +const { Dot } = require('../dot.js'); + +// Helper function to create test kernel +function createKernel(data, context = null) { + const kernel = new DotKernel(context); + for (const [dotStr, value] of Object.entries(data)) { + kernel.dataStorage.set(dotStr, value); + kernel.sharedContext.insertDot(dotStr); + } + return kernel; +} + +describe('DotKernel', () => { + describe('Constructor & Context Management', () => { + it('should create kernel with own base context', () => { + const kernel = new DotKernel(); + assert.ok(kernel.sharedContext === kernel.contextBase); + assert.ok(kernel.sharedContext instanceof DotContext); + }); + + it('should create kernel with shared context', () => { + const externalCtx = new DotContext(); + const kernel = new DotKernel(externalCtx); + assert.ok(kernel.sharedContext === externalCtx); + assert.ok(kernel.contextBase !== externalCtx); + }); + + it('should allow multiple kernels to share same context', () => { + const sharedCtx = new DotContext(); + const kernel1 = new DotKernel(sharedCtx); + const kernel2 = new DotKernel(sharedCtx); + + kernel1.add('a', 'value1'); + + assert.ok(kernel1.sharedContext === kernel2.sharedContext); + assert.equal(sharedCtx.compactCausalContext.get('a'), 1); + }); + + it('should initialize with empty data storage', () => { + const kernel = new DotKernel(); + assert.equal(kernel.dataStorage.size, 0); + }); + }); + + describe('add()', () => { + it('should add value with new dot', () => { + const kernel = new DotKernel(); + kernel.add('replicaA', 'value1'); + assert.ok(kernel.dataStorage.has('replicaA:1')); + assert.equal(kernel.dataStorage.get('replicaA:1'), 'value1'); + }); + + it('should return delta kernel with just the added dot', () => { + const kernel = new DotKernel(); + const delta = kernel.add('a', 'val'); + assert.equal(delta.dataStorage.size, 1); + assert.ok(delta.dataStorage.has('a:1')); + assert.equal(delta.dataStorage.get('a:1'), 'val'); + assert.equal(delta.sharedContext.dotIn({ id: 'a', counter: 1 }), true); + }); + + it('should increment counter for same replica', () => { + const kernel = new DotKernel(); + kernel.add('a', 'v1'); + kernel.add('a', 'v2'); + assert.ok(kernel.dataStorage.has('a:1')); + assert.ok(kernel.dataStorage.has('a:2')); + assert.equal(kernel.dataStorage.get('a:1'), 'v1'); + assert.equal(kernel.dataStorage.get('a:2'), 'v2'); + }); + + it('should update shared context', () => { + const sharedCtx = new DotContext(); + const kernel = new DotKernel(sharedCtx); + kernel.add('a', 'val'); + assert.equal(sharedCtx.compactCausalContext.get('a'), 1); + }); + + it('should handle multiple replicas independently', () => { + const kernel = new DotKernel(); + kernel.add('a', 'x'); + kernel.add('b', 'y'); + kernel.add('a', 'z'); + assert.equal(kernel.dataStorage.get('a:1'), 'x'); + assert.equal(kernel.dataStorage.get('b:1'), 'y'); + assert.equal(kernel.dataStorage.get('a:2'), 'z'); + }); + + it('should add complex values', () => { + const kernel = new DotKernel(); + const obj = { key: 'value', nested: { data: 123 } }; + kernel.add('a', obj); + assert.deepEqual(kernel.dataStorage.get('a:1'), obj); + }); + + it('should add null values', () => { + const kernel = new DotKernel(); + kernel.add('a', null); + assert.equal(kernel.dataStorage.get('a:1'), null); + }); + }); + + describe('dotAdd()', () => { + it('should add value and return dot', () => { + const kernel = new DotKernel(); + const dot = kernel.dotAdd('a', 'val'); + assert.ok(dot instanceof Dot); + assert.equal(dot.id, 'a'); + assert.equal(dot.counter, 1); + assert.equal(kernel.dataStorage.get('a:1'), 'val'); + }); + + it('should not return delta kernel', () => { + const kernel = new DotKernel(); + const result = kernel.dotAdd('a', 'val'); + assert.ok(!(result instanceof DotKernel)); + assert.ok(result instanceof Dot); + }); + + it('should increment counter like add()', () => { + const kernel = new DotKernel(); + const dot1 = kernel.dotAdd('a', 'v1'); + const dot2 = kernel.dotAdd('a', 'v2'); + assert.equal(dot1.counter, 1); + assert.equal(dot2.counter, 2); + }); + }); + + describe('rmv()', () => { + describe('Remove by value', () => { + it('should remove all matching dots', () => { + const kernel = createKernel({ + 'a:1': 'x', + 'b:1': 'x', + 'c:1': 'y', + }); + kernel.rmv('x'); + assert.ok(!kernel.dataStorage.has('a:1')); + assert.ok(!kernel.dataStorage.has('b:1')); + assert.ok(kernel.dataStorage.has('c:1')); + }); + + it('should return delta with removed dots in context', () => { + const kernel = createKernel({ 'a:1': 'x' }); + const delta = kernel.rmv('x'); + assert.equal(delta.sharedContext.dotIn({ id: 'a', counter: 1 }), true); + assert.equal(delta.dataStorage.size, 0); + }); + + it('should not remove if value not found', () => { + const kernel = createKernel({ 'a:1': 'x' }); + const delta = kernel.rmv('y'); + assert.ok(kernel.dataStorage.has('a:1')); + assert.equal(delta.dataStorage.size, 0); + }); + + it('should handle complex value comparison', () => { + const obj = { key: 'value' }; + const kernel = createKernel({ 'a:1': obj }); + kernel.rmv({ key: 'value' }); + assert.ok(!kernel.dataStorage.has('a:1')); + }); + }); + + describe('Remove by dot', () => { + it('should remove specific dot', () => { + const kernel = createKernel({ 'a:1': 'x', 'a:2': 'y' }); + kernel.rmv({ id: 'a', counter: 1 }); + assert.ok(!kernel.dataStorage.has('a:1')); + assert.ok(kernel.dataStorage.has('a:2')); + }); + + it('should handle not found gracefully', () => { + const kernel = createKernel({ 'a:1': 'x' }); + const delta = kernel.rmv({ id: 'b', counter: 1 }); + assert.ok(kernel.dataStorage.has('a:1')); + assert.equal(delta.dataStorage.size, 0); + }); + + it('should remove by dot-like object with string keys', () => { + const kernel = createKernel({ 'a:1': 'x', 'a:2': 'y' }); + kernel.rmv({ id: 'a', counter: 1 }); + assert.ok(!kernel.dataStorage.has('a:1')); + assert.ok(kernel.dataStorage.has('a:2')); + }); + + it('should accept Dot instance', () => { + const kernel = createKernel({ 'a:1': 'x', 'a:2': 'y' }); + const dot = new Dot('a', 1); + kernel.rmv(dot); + assert.ok(!kernel.dataStorage.has('a:1')); + assert.ok(kernel.dataStorage.has('a:2')); + }); + }); + + describe('Remove all', () => { + it('should remove all dots', () => { + const kernel = createKernel({ 'a:1': 'x', 'b:1': 'y', 'a:2': 'z' }); + kernel.rmv(); + assert.equal(kernel.dataStorage.size, 0); + }); + + it('should remember all removed dots in delta', () => { + const kernel = createKernel({ 'a:1': 'x', 'b:1': 'y' }); + const delta = kernel.rmv(); + assert.equal(delta.sharedContext.dotIn({ id: 'a', counter: 1 }), true); + assert.equal(delta.sharedContext.dotIn({ id: 'b', counter: 1 }), true); + }); + + it('should handle empty kernel', () => { + const kernel = new DotKernel(); + const delta = kernel.rmv(); + assert.equal(kernel.dataStorage.size, 0); + assert.equal(delta.dataStorage.size, 0); + }); + }); + }); + + describe('join()', () => { + it('should merge disjoint dot sets', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const k2 = createKernel({ 'b:1': 'y' }); + k1.join(k2); + assert.ok(k1.dataStorage.has('a:1')); + assert.ok(k1.dataStorage.has('b:1')); + assert.equal(k1.dataStorage.get('a:1'), 'x'); + assert.equal(k1.dataStorage.get('b:1'), 'y'); + }); + + it('should keep dots present in both kernels', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const k2 = createKernel({ 'a:1': 'x' }); + k1.join(k2); + assert.ok(k1.dataStorage.has('a:1')); + assert.equal(k1.dataStorage.size, 1); + }); + + it('should remove dots known by other context (observed remove)', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const k2 = new DotKernel(); + k2.sharedContext.insertDot({ id: 'a', counter: 1 }); + k1.join(k2); + assert.ok(!k1.dataStorage.has('a:1')); + }); + + it('should add dots from other not in this context', () => { + const k1 = new DotKernel(); + const k2 = createKernel({ 'b:5': 'new' }); + k1.join(k2); + assert.ok(k1.dataStorage.has('b:5')); + assert.equal(k1.dataStorage.get('b:5'), 'new'); + }); + + it('should not import dots this context already knows', () => { + const k1 = new DotKernel(); + k1.sharedContext.insertDot({ id: 'a', counter: 3 }); + const k2 = createKernel({ 'a:3': 'x' }); + k1.join(k2); + assert.ok(!k1.dataStorage.has('a:3')); + }); + + it('should join contexts', () => { + const k1 = new DotKernel(); + k1.sharedContext.compactCausalContext.set('a', 3); + const k2 = new DotKernel(); + k2.sharedContext.compactCausalContext.set('a', 5); + k1.join(k2); + assert.equal(k1.sharedContext.compactCausalContext.get('a'), 5); + }); + + it('should be idempotent', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const k2 = createKernel({ 'b:1': 'y' }); + k1.join(k2); + const size1 = k1.dataStorage.size; + k1.join(k2); + const size2 = k1.dataStorage.size; + assert.equal(size1, size2); + }); + + it('should handle self-join gracefully', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const before = k1.dataStorage.size; + k1.join(k1); + assert.equal(k1.dataStorage.size, before); + }); + + it('should use sorted iteration', () => { + const k1 = createKernel({ 'b:1': 'y', 'a:1': 'x' }); + const k2 = createKernel({ 'c:1': 'z' }); + k1.join(k2); + assert.equal(k1.dataStorage.size, 3); + }); + + it('should handle complex concurrent scenario', () => { + const k1 = new DotKernel(); + k1.add('a', 'v1'); + k1.add('a', 'v2'); + + const k2 = new DotKernel(); + k2.add('a', 'v3'); + + k1.join(k2); + + // k1 has (a:1) and (a:2) with CC[a]=2 + // k2 has (a:1) with CC[a]=1 + // When joining, k2's (a:1) is already known by k1's context + assert.ok(k1.dataStorage.has('a:1')); + assert.ok(k1.dataStorage.has('a:2')); + // k2's (a:1) is not imported because k1 already knows about (a:1) + assert.equal(k1.dataStorage.size, 2); + }); + + it('should handle complex concurrent scenarios with deltas', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const k2 = createKernel({ 'a:1': 'y' }); + const delta1 = k1.add('a', 'v1'); + const delta2 = k1.add('a', 'v2'); + k2.join(delta1); + k2.join(delta2); + + assert.equal(k2.dataStorage.size, 3); + assert.equal(k2.dataStorage.get('a:1'), 'y'); + assert.equal(k2.dataStorage.get('a:2'), 'v1'); + assert.equal(k2.dataStorage.get('a:3'), 'v2'); + }); + }); + + describe('deepJoin()', () => { + it('should merge values for dots in both kernels', () => { + const k1 = createKernel({ 'a:1': 5 }); + const k2 = createKernel({ 'a:1': 3 }); + k1.deepJoin(k2); + assert.ok(k1.dataStorage.has('a:1')); + assert.equal(k1.dataStorage.get('a:1'), 5); + }); + + it('should only join when values differ', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const k2 = createKernel({ 'a:1': 'x' }); + k1.deepJoin(k2); + assert.equal(k1.dataStorage.get('a:1'), 'x'); + }); + + it('should behave like join() for non-overlapping dots', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const k2 = createKernel({ 'b:1': 'y' }); + k1.deepJoin(k2); + assert.ok(k1.dataStorage.has('a:1')); + assert.ok(k1.dataStorage.has('b:1')); + }); + + it('should handle nested CRDTs with join method', () => { + const crdt1 = { + value: 5, + join(other) { + this.value = Math.max(this.value, other.value); + }, + }; + const crdt2 = { + value: 8, + join(other) { + this.value = Math.max(this.value, other.value); + }, + }; + const k1 = createKernel({ 'a:1': crdt1 }); + const k2 = createKernel({ 'a:1': crdt2 }); + k1.deepJoin(k2); + assert.ok(k1.dataStorage.has('a:1')); + }); + + // eslint-disable-next-line max-len + it('should remove dots known by other context, and removed in other', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const k2 = new DotKernel(); + k2.sharedContext.insertDot({ id: 'a', counter: 1 }); + k1.deepJoin(k2); + assert.ok(!k1.dataStorage.has('a:1')); + }); + + it('should not import dots this context already knows', () => { + const k1 = new DotKernel(); + k1.sharedContext.insertDot({ id: 'a', counter: 3 }); + const k2 = createKernel({ 'a:3': 'x' }); + k1.deepJoin(k2); + assert.ok(!k1.dataStorage.has('a:3')); + }); + + it('should handle self-deepJoin gracefully', () => { + const k1 = createKernel({ 'a:1': 'x' }); + const before = k1.dataStorage.size; + k1.deepJoin(k1); + assert.equal(k1.dataStorage.size, before); + }); + }); + + describe('clone()', () => { + it('should create independent copy', () => { + const kernel = createKernel({ 'a:1': 'x' }); + const copy = kernel.clone(); + + copy.dataStorage.set('b:1', 'y'); + + assert.ok(!kernel.dataStorage.has('b:1')); + assert.ok(copy.dataStorage.has('b:1')); + }); + + it('should clone with local context', () => { + const kernel = new DotKernel(); + kernel.add('a', 'x'); + const copy = kernel.clone(); + + assert.ok(copy.sharedContext !== kernel.sharedContext); + assert.equal( + copy.sharedContext.compactCausalContext.get('a'), + kernel.sharedContext.compactCausalContext.get('a'), + ); + }); + + it('should preserve shared context reference', () => { + const sharedCtx = new DotContext(); + const kernel = new DotKernel(sharedCtx); + kernel.add('a', 'x'); + const copy = kernel.clone(); + + assert.ok(copy.sharedContext === kernel.sharedContext); + assert.ok(copy.sharedContext === sharedCtx); + }); + + it('should clone empty kernel', () => { + const kernel = new DotKernel(); + const copy = kernel.clone(); + assert.equal(copy.dataStorage.size, 0); + }); + }); + + describe('toString()', () => { + it('should show kernel state', () => { + const kernel = createKernel({ 'a:1': 'x' }); + const str = kernel.toString(); + assert.ok(str.includes('Kernel')); + assert.ok(str.includes('a:1')); + assert.ok(str.includes('x')); + }); + + it('should show empty kernel', () => { + const kernel = new DotKernel(); + const str = kernel.toString(); + assert.ok(str.includes('Kernel')); + }); + + it('should show context information', () => { + const kernel = new DotKernel(); + kernel.add('a', 'x'); + const str = kernel.toString(); + assert.ok(str.includes('Context')); + }); + }); +}); diff --git a/DeltaBased/CausalContext/__tests__/dot.test.js b/DeltaBased/CausalContext/__tests__/dot.test.js new file mode 100644 index 0000000..060ad66 --- /dev/null +++ b/DeltaBased/CausalContext/__tests__/dot.test.js @@ -0,0 +1,228 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { Dot } = require('../dot.js'); + +describe('Dot', () => { + describe('Construction', () => { + it('should create dot with id and counter', () => { + const dot = new Dot('replica1', 5); + assert.equal(dot.id, 'replica1'); + assert.equal(dot.counter, 5); + }); + + it('should create dot from string "id:counter"', () => { + const dot = Dot.fromString('a:10'); + assert.equal(dot.id, 'a'); + assert.equal(dot.counter, 10); + }); + + it('should create dot from object literal', () => { + const obj = { id: 'b', counter: 3 }; + const dot = Dot.toDot(obj); + assert.ok(dot instanceof Dot); + assert.equal(dot.id, 'b'); + assert.equal(dot.counter, 3); + }); + + it('should accept Dot instance in toDot', () => { + const original = new Dot('c', 7); + const dot = Dot.toDot(original); + assert.equal(dot, original); + }); + + it('should reject null input', () => { + assert.throws(() => Dot.toDot(null), { + name: 'Error', + message: /Invalid dot input/, + }); + }); + + it('should reject undefined input', () => { + assert.throws(() => Dot.toDot(undefined), { + name: 'Error', + message: /Invalid dot input/, + }); + }); + + it('should reject empty object', () => { + assert.throws(() => Dot.toDot({}), { + name: 'Error', + message: /Invalid dot input/, + }); + }); + + it('should reject invalid string format', () => { + assert.throws(() => Dot.fromString('invalid'), Error); + }); + + it('should reject string with non-numeric counter', () => { + assert.throws(() => Dot.fromString('a:b'), { + name: 'Error', + message: /Invalid dot format/, + }); + }); + }); + + describe('Comparison', () => { + it('should compare dots with same id, different counters', () => { + const dot1 = new Dot('a', 1); + const dot2 = new Dot('a', 3); + const result = Dot.compare(dot1, dot2); + assert.ok(result < 0, 'dot1 should be less than dot2'); + }); + + it('should compare dots with different ids', () => { + const dot1 = new Dot('a', 5); + const dot2 = new Dot('b', 3); + const result = Dot.compare(dot1, dot2); + assert.ok(result < 0, 'a should be less than b lexicographically'); + }); + + it('should compare dots with same id and counter (equality)', () => { + const dot1 = new Dot('a', 5); + const dot2 = new Dot('a', 5); + const result = Dot.compare(dot1, dot2); + assert.equal(result, 0); + }); + + it('should handle reverse comparison', () => { + const dot1 = new Dot('a', 3); + const dot2 = new Dot('a', 1); + const result = Dot.compare(dot1, dot2); + assert.ok(result > 0, 'dot1 should be greater than dot2'); + }); + + it('should compare dots with strings as input', () => { + const result = Dot.compare('a:1', 'a:3'); + assert.ok(result < 0); + }); + + it('should compare dots with mixed formats', () => { + const dot1 = new Dot('a', 1); + const result = Dot.compare(dot1, 'a:3'); + assert.ok(result < 0); + }); + + it('should use lt() method correctly', () => { + const dot1 = new Dot('a', 1); + const dot2 = new Dot('a', 3); + assert.equal(dot1.lt(dot2), true); + assert.equal(dot2.lt(dot1), false); + }); + + it('should use lte() method correctly', () => { + const dot1 = new Dot('a', 1); + const dot2 = new Dot('a', 3); + const dot3 = new Dot('a', 1); + assert.equal(dot1.lte(dot2), true); + assert.equal(dot1.lte(dot3), true); + assert.equal(dot2.lte(dot1), false); + }); + + it('should use gt() method correctly', () => { + const dot1 = new Dot('a', 3); + const dot2 = new Dot('a', 1); + assert.equal(dot1.gt(dot2), true); + assert.equal(dot2.gt(dot1), false); + }); + + it('should use gte() method correctly', () => { + const dot1 = new Dot('a', 3); + const dot2 = new Dot('a', 1); + const dot3 = new Dot('a', 3); + assert.equal(dot1.gte(dot2), true); + assert.equal(dot1.gte(dot3), true); + assert.equal(dot2.gte(dot1), false); + }); + }); + + describe('Serialization', () => { + it('should convert dot to string format', () => { + const dot = new Dot('replica1', 42); + assert.equal(dot.toString(), 'replica1:42'); + }); + + it('should parse string back to dot', () => { + const dot = Dot.fromString('x:100'); + assert.equal(dot.id, 'x'); + assert.equal(dot.counter, 100); + }); + + it('should support round-trip conversion', () => { + const original = new Dot('test', 99); + const dotStr = original.toString(); + const restored = Dot.fromString(dotStr); + assert.equal(restored.id, original.id); + assert.equal(restored.counter, original.counter); + }); + + it('should use static toString with Dot instance', () => { + const dot = new Dot('a', 5); + assert.equal(Dot.toString(dot), 'a:5'); + }); + + it('should use static toString with string', () => { + assert.equal(Dot.toString('b:10'), 'b:10'); + }); + + it('should use static toString with object literal', () => { + assert.equal(Dot.toString({ id: 'c', counter: 15 }), 'c:15'); + }); + }); + + describe('isDotLikeObject', () => { + it('should return true for valid dot-like object', () => { + assert.equal(Dot.isDotLikeObject({ id: 'a', counter: 1 }), true); + }); + + it('should return false for object without id', () => { + assert.equal(Dot.isDotLikeObject({ counter: 1 }), false); + }); + + it('should return false for object without counter', () => { + assert.equal(Dot.isDotLikeObject({ id: 'a' }), false); + }); + + it('should return false for non-object', () => { + assert.equal(Dot.isDotLikeObject('a:1'), false); + assert.equal(Dot.isDotLikeObject(null), false); + assert.equal(Dot.isDotLikeObject(undefined), false); + }); + + it('should return true for Dot instance', () => { + const dot = new Dot('a', 1); + assert.equal(Dot.isDotLikeObject(dot), true); + }); + }); + + describe('Edge Cases', () => { + it('should handle dots with special characters in id', () => { + const dot = new Dot('replica-1_test', 5); + assert.equal(dot.toString(), 'replica-1_test:5'); + const restored = Dot.fromString(dot.toString()); + assert.equal(restored.id, 'replica-1_test'); + }); + + it('should handle large counter values', () => { + const largeCounter = Number.MAX_SAFE_INTEGER - 1; + const dot = new Dot('a', largeCounter); + assert.equal(dot.counter, largeCounter); + }); + + it('should handle zero counter', () => { + const dot = new Dot('a', 0); + assert.equal(dot.counter, 0); + assert.equal(dot.toString(), 'a:0'); + }); + + it('should compare dots with same id prefix', () => { + const dot1 = new Dot('replica', 1); + const dot2 = new Dot('replica1', 1); + const result = Dot.compare(dot1, dot2); + assert.ok(result < 0, 'replica should be less than replica1'); + }); + }); +}); diff --git a/DeltaBased/CausalContext/delta-helpers.js b/DeltaBased/CausalContext/delta-helpers.js new file mode 100644 index 0000000..17e3b14 --- /dev/null +++ b/DeltaBased/CausalContext/delta-helpers.js @@ -0,0 +1,20 @@ +'use strict'; + +function join(left, right) { + if (typeof left === 'number' && typeof right === 'number') { + return Math.max(left, right); + } + if (left && typeof left.join === 'function') { + const res = Object.create(Object.getPrototypeOf(left)); + Object.assign(res, left); + res.join(right); + return res; + } + throw new Error('Cannot join these types'); +} + +const DeltaHelpers = { + join, +}; + +module.exports = { DeltaHelpers }; diff --git a/DeltaBased/CausalContext/dot-context.js b/DeltaBased/CausalContext/dot-context.js new file mode 100644 index 0000000..328b394 --- /dev/null +++ b/DeltaBased/CausalContext/dot-context.js @@ -0,0 +1,114 @@ +'use strict'; +// Dot Context - Autonomous causal context for context sharing + +const { Dot } = require('./dot'); + +class DotContext { + constructor(compactCausalContext = new Map(), dotCloud = new Set()) { + // Map + // Map + this.compactCausalContext = compactCausalContext; // Compact causal context + // Set + // Set<"id:counter"> + this.dotCloud = dotCloud; // Dot cloud (stored as strings "id:counter") + } + + // refactor + toString() { + const cc = Array.from(this.compactCausalContext.entries()) + .map(([k, v]) => new Dot(k, v).toString()) + .join(' '); + const dc = Array.from(this.dotCloud).join(' '); + + return `Context: CC (${cc}) DC (${dc})`; + } + + dotIn(dot) { + // Check if dot is in context + const dotObj = Dot.toDot(dot); + + const ccValue = this.compactCausalContext.get(dotObj.id); + + if (ccValue !== undefined && dotObj.counter <= ccValue) { + return true; + } + + const dotStr = dotObj.toString(); + return this.dotCloud.has(dotStr); + } + + compact() { + let compactedCloud; + do { + compactedCloud = false; + const toDelete = []; + + for (const cloudDotStr of this.dotCloud) { + const cloudDot = Dot.fromString(cloudDotStr); + const contextDotCounter = this.compactCausalContext.get(cloudDot.id); + + if (contextDotCounter === undefined) { + if (cloudDot.counter === 1) { + this.compactCausalContext.set(cloudDot.id, cloudDot.counter); + toDelete.push(cloudDotStr); + compactedCloud = true; + } + } else if (cloudDot.counter === contextDotCounter + 1) { + this.compactCausalContext.set(cloudDot.id, contextDotCounter + 1); + toDelete.push(cloudDotStr); + compactedCloud = true; + } else if (cloudDot.counter <= contextDotCounter) { + toDelete.push(cloudDotStr); + } + } + + for (const dotStr of toDelete) { + this.dotCloud.delete(dotStr); + } + } while (compactedCloud); + } + + makeDot(id) { + const current = this.compactCausalContext.get(id) || 0; + const newCounter = current + 1; + this.compactCausalContext.set(id, newCounter); + return new Dot(id, newCounter); + } + + insertDot(dot, compactNow = true) { + const dotStr = Dot.toString(dot); + this.dotCloud.add(dotStr); + if (compactNow) { + this.compact(); + } + } + + join(otherContext) { + if (this === otherContext) return; + + // Join Compact Causal Context + for (const [id, value] of otherContext.compactCausalContext) { + const current = this.compactCausalContext.get(id); + this.compactCausalContext.set( + id, + current === undefined ? value : Math.max(current, value), + ); + } + + // Join Dot Cloud + for (const dot of otherContext.dotCloud) { + this.insertDot(dot, false); + } + + this.compact(); + } + + clone() { + return new DotContext( + new Map(this.compactCausalContext), + new Set(this.dotCloud), + ); + } +} + +module.exports = { DotContext }; diff --git a/DeltaBased/CausalContext/dot-kernel.js b/DeltaBased/CausalContext/dot-kernel.js new file mode 100644 index 0000000..263121b --- /dev/null +++ b/DeltaBased/CausalContext/dot-kernel.js @@ -0,0 +1,189 @@ +'use strict'; +// Dot Kernel - Core kernel for dot-based CRDTs + +const { DotContext } = require('./dot-context'); +const { Dot } = require('./dot'); +const { DeltaHelpers } = require('./delta-helpers'); + +class DotKernel { + constructor( + sharedContext = null, + dataStorage = new Map(), + contextBase = new DotContext(), + ) { + // Map of dots to values (dot stored as "id:counter") + // Map + this.dataStorage = dataStorage; + this.contextBase = contextBase; + this.sharedContext = sharedContext || this.contextBase; + } + + toString() { + const dotValues = Array.from(this.dataStorage.entries()) + .map(([dotStr, val]) => `${dotStr}->${JSON.stringify(val)}`) + .join(' '); + + const sharedContextStr = this.sharedContext.toString(); + + return `Kernel: DS (${dotValues}) ${sharedContextStr}`; + } + + join(otherKernel) { + if (this === otherKernel) return; + + const thisDots = Array.from(this.dataStorage.keys()).sort(Dot.compare); + const otherDots = Array.from(otherKernel.dataStorage.keys()).sort( + Dot.compare, + ); + + let i = 0; + let j = 0; + + while (i < thisDots.length || j < otherDots.length) { + const thisDot = i < thisDots.length ? thisDots[i] : null; + const otherDot = j < otherDots.length ? otherDots[j] : null; + + if (thisDot !== null && (otherDot === null || thisDot < otherDot)) { + // Dot only in this + if (otherKernel.sharedContext.dotIn(thisDot)) { + // other knows dot, must delete here + this.dataStorage.delete(thisDot); + } + // keep it + i++; + } else if ( + otherDot !== null && + (thisDot === null || Dot.compare(otherDot, thisDot) < 0) + ) { + // Dot only in other + if (!this.sharedContext.dotIn(otherDot)) { + this.dataStorage.set(otherDot, otherKernel.dataStorage.get(otherDot)); + } + j++; + } else { + // Dot in both + i++; + j++; + } + } + + this.sharedContext.join(otherKernel.sharedContext); + } + + deepJoin(other) { + if (this === other) return; + + const thisDots = Array.from(this.dataStorage.keys()).sort(Dot.compare); + const otherDots = Array.from(other.dataStorage.keys()).sort(Dot.compare); + + let i = 0; + let j = 0; + + while (i < thisDots.length || j < otherDots.length) { + const thisDot = i < thisDots.length ? thisDots[i] : null; + const otherDot = j < otherDots.length ? otherDots[j] : null; + + if (thisDot !== null && (otherDot === null || thisDot < otherDot)) { + // Dot only in this + if (other.sharedContext.dotIn(thisDot)) { + this.dataStorage.delete(thisDot); + } + i++; + } else if ( + otherDot !== null && + (thisDot === null || otherDot < thisDot) + ) { + // Dot only in other + if (!this.sharedContext.dotIn(otherDot)) { + this.dataStorage.set(otherDot, other.dataStorage.get(otherDot)); + } + j++; + } else { + // Dot in both - join the payloads + const thisVal = this.dataStorage.get(thisDot); + const otherVal = other.dataStorage.get(otherDot); + if (JSON.stringify(thisVal) !== JSON.stringify(otherVal)) { + this.dataStorage.set(thisDot, DeltaHelpers.join(thisVal, otherVal)); + } + i++; + j++; + } + } + + this.sharedContext.join(other.sharedContext); + } + + add(id, val) { + const res = new DotKernel(); + const dot = this.sharedContext.makeDot(id); + const dotStr = Dot.toString(dot); + + this.dataStorage.set(dotStr, val); + res.dataStorage.set(dotStr, val); + res.sharedContext.insertDot(dot); + + return res; + } + + dotAdd(id, val) { + const dot = this.sharedContext.makeDot(id); + const dotStr = Dot.toString(dot); + this.dataStorage.set(dotStr, val); + return dot; + } + + rmv(val = null) { + const res = new DotKernel(); + + if (val === null) { + // Remove all dots + for (const [dotStr] of this.dataStorage) { + res.sharedContext.insertDot(dotStr, false); + } + res.sharedContext.compact(); + this.dataStorage.clear(); + } else if (Dot.isDotLikeObject(val)) { + // Remove specific dot + const dotStr = Dot.toString(val); + if (this.dataStorage.has(dotStr)) { + res.sharedContext.insertDot(dotStr, false); + this.dataStorage.delete(dotStr); + } + res.sharedContext.compact(); + } else { + // Remove all dots matching value + const toDelete = []; + for (const [dotStr, v] of this.dataStorage) { + if (JSON.stringify(v) === JSON.stringify(val)) { + res.sharedContext.insertDot(dotStr, false); + toDelete.push(dotStr); + } + } + for (const dotStr of toDelete) { + this.dataStorage.delete(dotStr); + } + res.sharedContext.compact(); + } + + return res; + } + + clone() { + if (this.sharedContext === this.contextBase) { + const clonedContext = this.contextBase.clone(); + return new DotKernel( + clonedContext, + new Map(this.dataStorage), + clonedContext, + ); + } + return new DotKernel( + // we need to keep the shared context to keep the link + this.sharedContext, + new Map(this.dataStorage), + this.contextBase.clone(), + ); + } +} + +module.exports = { DotKernel }; diff --git a/DeltaBased/CausalContext/dot.js b/DeltaBased/CausalContext/dot.js new file mode 100644 index 0000000..63d8fea --- /dev/null +++ b/DeltaBased/CausalContext/dot.js @@ -0,0 +1,84 @@ +'use strict'; + +class Dot { + constructor(id, counter) { + this.id = id; + this.counter = counter; + } + + toString() { + return `${this.id}:${this.counter}`; + } + + gt(other) { + return Dot.compare(this, other) > 0; + } + + gte(other) { + return Dot.compare(this, other) >= 0; + } + + lt(other) { + return Dot.compare(this, other) < 0; + } + + lte(other) { + return Dot.compare(this, other) <= 0; + } + + static fromString(dotStr) { + if (typeof dotStr !== 'string') { + throw new Error( + 'Invalid dot input expected to be string: ' + JSON.stringify(dotStr), + ); + } + const parts = dotStr.split(':'); + if (parts.length !== 2) { + throw new Error( + 'Invalid dot format: expected "id:counter", got "' + dotStr + '"', + ); + } + const [id, counterStr] = parts; + const counter = parseInt(counterStr); + if (isNaN(counter) || !id) { + throw new Error( + 'Invalid dot format: id must be non-empty ' + + 'string and counter must be an integer, got "' + + dotStr + + '"', + ); + } + return new Dot(id, counter); + } + + static toDot(dot) { + if (typeof dot === 'string') return Dot.fromString(dot); + if (dot instanceof Dot) return dot; + if (Dot.isDotLikeObject(dot)) { + return new Dot(dot.id, dot.counter); + } + throw new Error('Invalid dot input: ' + JSON.stringify(dot)); + } + + static isDotLikeObject(dot) { + return ( + typeof dot === 'object' && + dot !== null && + dot.id !== undefined && + dot.counter !== undefined + ); + } + + static toString(dot) { + return Dot.toDot(dot).toString(); + } + + static compare(_dot1, _dot2) { + const dot1 = Dot.toDot(_dot1); + const dot2 = Dot.toDot(_dot2); + if (dot1.id !== dot2.id) return dot1.id.localeCompare(dot2.id); + return dot1.counter - dot2.counter; + } +} + +module.exports = { Dot }; diff --git a/package-lock.json b/package-lock.json index 4d25b1a..3b8e1d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,16 +7,18 @@ "": { "name": "patterns", "version": "1.0.0", - "dependencies": { + "devDependencies": { "eslint": "^9.12.0", "eslint-config-metarhia": "^9.1.1", - "prettier": "^3.3.3" + "prettier": "^3.3.3", + "typescript": "^5.9.3" } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -35,6 +37,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -47,6 +50,7 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -56,6 +60,7 @@ "version": "0.21.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -70,6 +75,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -79,6 +85,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -91,6 +98,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -114,6 +122,7 @@ "version": "9.31.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -126,6 +135,7 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -135,6 +145,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.15.1", @@ -148,6 +159,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -157,6 +169,7 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", @@ -170,6 +183,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -183,6 +197,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -196,6 +211,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -209,6 +225,7 @@ "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" @@ -221,18 +238,21 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -245,6 +265,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -254,6 +275,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -270,6 +292,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -285,18 +308,21 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -307,6 +333,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -316,6 +343,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -332,6 +360,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -344,18 +373,21 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -370,6 +402,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -387,12 +420,14 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -405,6 +440,7 @@ "version": "9.31.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -465,6 +501,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-config-metarhia/-/eslint-config-metarhia-9.1.2.tgz", "integrity": "sha512-KSt3eYbM4sII4+v0Tb1D1krGAtKSnfZ/2ndPNdDEdlSjT19ga/mZ7Y5q6E9HHkFKmmlRA/1XYw0ZkIeAj4rMEw==", + "dev": true, "license": "MIT", "dependencies": { "eslint": "^9.28.0", @@ -484,6 +521,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" @@ -496,6 +534,7 @@ "version": "5.5.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", + "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", @@ -526,6 +565,7 @@ "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -542,6 +582,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -554,6 +595,7 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", @@ -571,6 +613,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -583,6 +626,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -595,6 +639,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -604,6 +649,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -613,30 +659,35 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-diff": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, "license": "MIT" }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -649,6 +700,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -665,6 +717,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -678,12 +731,14 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, "license": "ISC" }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -696,6 +751,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -708,6 +764,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -717,6 +774,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -726,6 +784,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -742,6 +801,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -751,6 +811,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -760,6 +821,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -772,12 +834,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -790,24 +854,28 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -817,6 +885,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -830,6 +899,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -845,12 +915,14 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, "license": "MIT" }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -863,18 +935,21 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, "license": "MIT" }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -892,6 +967,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -907,6 +983,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -922,6 +999,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -934,6 +1012,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -943,6 +1022,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -952,6 +1032,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -961,6 +1042,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -976,6 +1058,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, "license": "MIT", "dependencies": { "fast-diff": "^1.1.2" @@ -988,6 +1071,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -997,6 +1081,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -1006,6 +1091,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -1018,6 +1104,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1027,6 +1114,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1039,6 +1127,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -1051,6 +1140,7 @@ "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, "license": "MIT", "dependencies": { "@pkgr/core": "^0.2.9" @@ -1066,6 +1156,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -1074,10 +1165,25 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -1087,6 +1193,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -1102,6 +1209,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1111,6 +1219,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 66f2af3..3541cb7 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,17 @@ "version": "1.0.0", "scripts": { "lint": "eslint . && prettier -c \"**/*.js\"", - "fix": "eslint . --fix && prettier --write \"**/*.js\"" + "fix": "eslint . --fix && prettier --write \"**/*.js\"", + "types": "tsc -p tsconfig.json", + "test": "node --test ./**/__tests__/**/*.test.js", + "test:coverage": "node --test --experimental-test-coverage ./**/__tests__/**/*.test.js" }, "author": "Timur Shemsedinov", "private": true, - "dependencies": { + "devDependencies": { "eslint": "^9.12.0", "eslint-config-metarhia": "^9.1.1", - "prettier": "^3.3.3" + "prettier": "^3.3.3", + "typescript": "^5.9.3" } }