diff --git a/.changeset/four-bottles-report.md b/.changeset/four-bottles-report.md new file mode 100644 index 0000000..34e9973 --- /dev/null +++ b/.changeset/four-bottles-report.md @@ -0,0 +1,5 @@ +--- +"memotable": minor +--- + +Reduce bundle size diff --git a/package.json b/package.json index c22adaa..438ffb2 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "release": "pnpm changeset version && pnpm install && git add . && git commit -m 'chore: version bump' && git push", "publish:ci": "pnpm changeset publish", "prepare": "husky install", - "size": "pnpm build && pnpm size-limit && node measure-size.ts memo-table Table && pnpm size:mangle", + "size": "pnpm build && pnpm size-limit && node measure-size.ts memotable Table && pnpm size:mangle", "size:mangle": "npx esbuild src/index.ts --bundle --minify --mangle-props='^_' --outfile=dist/mangled.js && brotli -c dist/mangled.js | wc -c", "benchmark": "vitest run Table.benchmark.test.ts --testTimeout=120000 --no-coverage", "ci": "pnpm format && pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm size" @@ -114,7 +114,7 @@ "name": "Table", "path": "dist/index.js", "import": "{ Table }", - "limit": "1500 B" + "limit": "1000 B" } ], "packageManager": "pnpm@10.26.2", diff --git a/src/Table.test.ts b/src/Table.test.ts index 8b9d6a7..9cb46ea 100644 --- a/src/Table.test.ts +++ b/src/Table.test.ts @@ -280,16 +280,16 @@ describe("Table", () => { table.set("1", { tags: ["A"] }); table.index((_) => ""); - expect(table.partitions().length).toEqual(0); + expect(Array.from(table.partitions()).length).toEqual(0); table.index((_) => null); - expect(table.partitions().length).toEqual(0); + expect(Array.from(table.partitions()).length).toEqual(0); table.index((_) => []); - expect(table.partitions().length).toEqual(0); + expect(Array.from(table.partitions()).length).toEqual(0); table.index((_) => ["VALID", "", null]); - expect(table.partitions().length).toEqual(1); + expect(Array.from(table.partitions()).length).toEqual(1); expect(table.partition("VALID").size).toEqual(1); }); @@ -304,7 +304,13 @@ describe("Table", () => { // Eagerly access a partition to create it table.partition("E").sort(() => 0); - expect(table.partitions().map(([key]) => key)).toEqual(["A", "B", "C", "D", "E"]); // Should include empty "E" partition + expect(Array.from(table.partitions()).map(([key]) => key)).toEqual([ + "A", + "B", + "C", + "D", + "E", + ]); // Should include empty "E" partition }); test("should update partitions correctly when values are added, updated or removed", () => { @@ -495,7 +501,7 @@ describe("Table", () => { const table = createTable(); table.set("1", { tags: ["A"] }); table.index(); - expect(table.partitions().length).toBe(0); + expect(Array.from(table.partitions()).length).toBe(0); }); test("nested indexing", () => { @@ -532,7 +538,7 @@ describe("Table", () => { table.partition(`partition${i}`); } - expect(table.partitions().length).toBe(1000); + expect(Array.from(table.partitions()).length).toBe(1000); // But table is still empty expect(table.size).toBe(0); @@ -1053,51 +1059,6 @@ describe("Table", () => { expect(subscriber).not.toHaveBeenCalled(); }); - test("should allow modifications via external reference in a batch (because it's not necessary and can make things complicated)", () => { - const table = createTable(); - table.set("1", { title: "Initial Task" }); - - expect(() => - table.batch(() => { - table.set("2", { title: "New task" }); - }), - ).toThrow(); - - expect(() => - table.batch(() => { - table.delete("1"); - }), - ).toThrow(); - - expect(() => - table.batch(() => { - table.touch("1"); - }), - ).toThrow(); - }); - - test("should not allow sort,index and memo via external reference in a batch (because these can't be reverted if batch fails)", () => { - const table = createTable(); - - expect(() => - table.batch(() => { - table.sort(() => 0); - }), - ).toThrow(); - - expect(() => - table.batch(() => { - table.index(() => ""); - }), - ).toThrow(); - - expect(() => - table.batch(() => { - table.memo(); - }), - ).toThrow(); - }); - test("subscriptions for batch operations", () => { const table = createTable(); diff --git a/src/Table.ts b/src/Table.ts index 1a63936..b2220bb 100644 --- a/src/Table.ts +++ b/src/Table.ts @@ -56,14 +56,9 @@ export class Table implements ITable { } public *entries(): MapIterator<[K, V]> { - const keys = this.keys(); - - do { - const next = keys.next(); - if (next.done) return; - - yield [next.value, this._map.get(next.value)!]; - } while (true); + for (const key of this.keys()) { + yield [key, this._map.get(key)!]; + } } public [Symbol.iterator](): MapIterator<[K, V]> { @@ -80,8 +75,6 @@ export class Table implements ITable { private _shouldMemoize: boolean = false; public memo(flag?: boolean): void { - this._throwIfBatchOperationInProgress(); - this._shouldMemoize = flag ?? true; // Step 1: Propagate memoization to all partitions @@ -90,7 +83,7 @@ export class Table implements ITable { } // Step 2: Refresh memoization for the current table - this._refreshMemoization(); + this._refreshMemoizedData(); } // #endregion @@ -128,45 +121,34 @@ export class Table implements ITable { // #region BATCHING - // Flag to indicate if a batch operation is in progress - private _isBatchOperationInProgress: boolean = false; - public batch(fn: (t: IBatch) => void): void { - // Step 1: Run the batch of operations and mark start and end to disable change propagation on every change - this._isBatchOperationInProgress = true; - // Tracks keys (and the new values) that have been updated in this batch const _updates = new Map(); // Tracks keys that have been deleted in this batch const _deletes = new Set(); - try { - fn({ - set: (key: K, value: V) => { - _updates.set(key, value); + fn({ + set: (key: K, value: V) => { + _updates.set(key, value); + _deletes.delete(key); // In case it was marked for deletion earlier + }, + delete: (key: K) => { + _updates.delete(key); // In case it was marked for update earlier + + // Only mark for deletion if the key exists in the target map + if (this._map.has(key)) { + _deletes.add(key); + } + }, + touch: (key: K): void => { + const currentValue = this._map.get(key); + if (currentValue !== undefined) { + _updates.set(key, currentValue); _deletes.delete(key); // In case it was marked for deletion earlier - }, - delete: (key: K) => { - _updates.delete(key); // In case it was marked for update earlier - - // Only mark for deletion if the key exists in the target map - if (this._map.has(key)) { - _deletes.add(key); - } - }, - touch: (key: K): void => { - const currentValue = this._map.get(key); - if (currentValue !== undefined) { - _updates.set(key, currentValue); - _deletes.delete(key); // In case it was marked for deletion earlier - } - }, - }); - } catch (e) { - this._isBatchOperationInProgress = false; - throw e; - } + } + }, + }); // Step 2: Apply all changes to the internal map and reset batch for (const [key, value] of _updates) { @@ -177,8 +159,6 @@ export class Table implements ITable { this._map.delete(key); } - this._isBatchOperationInProgress = false; - // Step 3: Propagate all changes as a batch const keys = [..._updates.keys(), ..._deletes]; if (keys.length > 0) { @@ -207,11 +187,7 @@ export class Table implements ITable { private _sortedValues: V[] | null = null; private _comparator: IComparator | null = null; - public sort(comparator: IComparator | null): void; - public sort(): void; public sort(comparator?: IComparator | null) { - this._throwIfBatchOperationInProgress(); - // If comparator is not provided, re-apply the existing comparator if (comparator === undefined) { comparator = this._comparator; @@ -225,7 +201,7 @@ export class Table implements ITable { } // Step 2: Refresh memoization based on the new comparator - this._refreshMemoization(); + this._refreshMemoizedData(); // Step 3: Notify subscribers because we fallback to internal map enforced order this._notifyListeners([]); @@ -247,18 +223,10 @@ export class Table implements ITable { /** All partitions created by this index */ private _partitions: Map> = new Map(); - public index( - definition: IIndexDefinition, - partitionInitializer?: (partition: IReadonlyTable, name: string) => void, - ): void; - public index(definition: null): void; - public index(): void; public index( definition?: IIndexDefinition | null, partitionInitializer?: (partition: IReadonlyTable, name: string) => void, ): void { - this._throwIfBatchOperationInProgress(); - // Step 1: Handle clearing the index if (definition === null) { this._indexAccessor = null; @@ -299,8 +267,8 @@ export class Table implements ITable { return this._getPartition(name); } - public partitions(): [string, IReadonlyTable][] { - return Array.from(this._partitions.entries()); + public partitions(): MapIterator<[string, IReadonlyTable]> { + return this._partitions.entries(); } // #endregion @@ -333,13 +301,11 @@ export class Table implements ITable { * @param updatedKeys Array of keys that have been updated */ private _propagateChanges(updatedKeys: Iterable): void { - this._throwIfBatchOperationInProgress(); - // Step 1: Update indexes if any this._applyIndexUpdate(updatedKeys); // Step 2: Update view - this._refreshMemoization(); + this._refreshMemoizedData(); // Step 3: Notify subscribers about the changes this._notifyListeners(updatedKeys); @@ -424,7 +390,7 @@ export class Table implements ITable { /** * Refresh the memoized data based on the current comparator and memoization flag. */ - private _refreshMemoization(): void { + private _refreshMemoizedData(): void { this._sortedKeys = this._sortedValues = null; // Table should be memoized when a comparator is set (otherwise memoization is not helpful) @@ -438,13 +404,6 @@ export class Table implements ITable { } } - /** Throw operation not allowed error if a batch operation is pending */ - private _throwIfBatchOperationInProgress() { - if (this._isBatchOperationInProgress) { - throw new Error("NotAllowed"); - } - } - // #endregion } diff --git a/src/contracts/IIndexableTable.ts b/src/contracts/IIndexableTable.ts index 66d7370..58fb67f 100644 --- a/src/contracts/IIndexableTable.ts +++ b/src/contracts/IIndexableTable.ts @@ -80,5 +80,5 @@ export interface IIndexableTable { /** * Get all (potentially empty) partitions in this table. */ - partitions(): [string, IReadonlyTable][]; + partitions(): MapIterator<[string, IReadonlyTable]>; }