kitcn 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aggregate/index.d.ts +7 -1
- package/dist/aggregate/index.js +1 -1
- package/dist/auth/generated/index.d.ts +1 -1
- package/dist/auth/index.d.ts +14 -14
- package/dist/cli.mjs +4 -2
- package/dist/{generated-contract-disabled-LOAfma7_.d.ts → generated-contract-disabled-BbPCefQM.d.ts} +12 -12
- package/dist/{local-env-DykABjCt.mjs → local-env-yFKub75x.mjs} +5 -1
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js +482 -161
- package/dist/{runtime-DmVSOe24.js → runtime-6DJxpDfb.js} +73 -37
- package/dist/watcher.mjs +1 -1
- package/dist/{where-clause-compiler-CmBz9CWA.d.ts → where-clause-compiler-BGBNBNit.d.ts} +9 -8
- package/package.json +1 -1
- package/skills/kitcn/references/features/aggregates.md +10 -1
|
@@ -1512,7 +1512,7 @@ function aggregateNameFromNamespace(namespace) {
|
|
|
1512
1512
|
async function insertHandler(ctx, args) {
|
|
1513
1513
|
const tree = await getOrCreateTree(ctx.db, args.namespace, DEFAULT_MAX_NODE_SIZE, true);
|
|
1514
1514
|
const summand = args.summand ?? 0;
|
|
1515
|
-
const pushUp = await insertIntoNode(ctx,
|
|
1515
|
+
const pushUp = await insertIntoNode(ctx, tree.maxNodeSize, tree.root, {
|
|
1516
1516
|
k: args.key,
|
|
1517
1517
|
v: args.value,
|
|
1518
1518
|
s: summand
|
|
@@ -1529,7 +1529,7 @@ async function insertHandler(ctx, args) {
|
|
|
1529
1529
|
}
|
|
1530
1530
|
async function deleteHandler(ctx, args) {
|
|
1531
1531
|
const tree = await getOrCreateTree(ctx.db, args.namespace, DEFAULT_MAX_NODE_SIZE, true);
|
|
1532
|
-
await deleteFromNode(ctx,
|
|
1532
|
+
await deleteFromNode(ctx, tree.maxNodeSize, tree.root, args.key);
|
|
1533
1533
|
const root = await ctx.db.get(tree.root);
|
|
1534
1534
|
if (root.items.length === 0 && root.subtrees.length === 1) {
|
|
1535
1535
|
log(`collapsing root ${root._id} because its only child is ${root.subtrees[0]}`);
|
|
@@ -1538,13 +1538,12 @@ async function deleteHandler(ctx, args) {
|
|
|
1538
1538
|
await ctx.db.delete(root._id);
|
|
1539
1539
|
}
|
|
1540
1540
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1541
|
+
function assertMaxNodeSize(maxNodeSize) {
|
|
1542
|
+
if (maxNodeSize % 2 !== 0 || maxNodeSize < 4) throw new Error("MAX_NODE_SIZE must be even and at least 4");
|
|
1543
1543
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
return max / 2;
|
|
1544
|
+
function minNodeSizeFor(maxNodeSize) {
|
|
1545
|
+
assertMaxNodeSize(maxNodeSize);
|
|
1546
|
+
return maxNodeSize / 2;
|
|
1548
1547
|
}
|
|
1549
1548
|
async function aggregateBetweenHandler(ctx, args) {
|
|
1550
1549
|
const tree = await getTree(ctx.db, args.namespace);
|
|
@@ -1554,8 +1553,8 @@ async function aggregateBetweenHandler(ctx, args) {
|
|
|
1554
1553
|
};
|
|
1555
1554
|
return await aggregateBetweenInNode(ctx.db, tree.root, args.k1, args.k2);
|
|
1556
1555
|
}
|
|
1557
|
-
async function filterBetween(db, node, k1, k2) {
|
|
1558
|
-
const n = await db.get(node);
|
|
1556
|
+
async function filterBetween(db, node, k1, k2, preloaded) {
|
|
1557
|
+
const n = preloaded ?? await db.get(node);
|
|
1559
1558
|
const included = [];
|
|
1560
1559
|
function includeSubtree(i, unboundedRight) {
|
|
1561
1560
|
const unboundedLeft = k1 === void 0 || included.length > 0;
|
|
@@ -1623,7 +1622,7 @@ async function offsetUntilHandler(ctx, args) {
|
|
|
1623
1622
|
namespace: args.namespace
|
|
1624
1623
|
})).count;
|
|
1625
1624
|
}
|
|
1626
|
-
async function deleteFromNode(ctx,
|
|
1625
|
+
async function deleteFromNode(ctx, maxNodeSize, node, key) {
|
|
1627
1626
|
let n = await ctx.db.get(node);
|
|
1628
1627
|
let foundItem = null;
|
|
1629
1628
|
let i = 0;
|
|
@@ -1659,13 +1658,13 @@ async function deleteFromNode(ctx, namespace, node, key) {
|
|
|
1659
1658
|
code: "DELETE_MISSING_KEY",
|
|
1660
1659
|
message: `key ${p(key)} not found in node ${n._id}`
|
|
1661
1660
|
});
|
|
1662
|
-
const deleted = await deleteFromNode(ctx,
|
|
1661
|
+
const deleted = await deleteFromNode(ctx, maxNodeSize, n.subtrees[i], key);
|
|
1663
1662
|
if (!deleted) return null;
|
|
1664
1663
|
if (!foundItem) foundItem = deleted;
|
|
1665
1664
|
const newAggregate = n.aggregate && sub(n.aggregate, itemAggregate(deleted));
|
|
1666
1665
|
if (newAggregate) await ctx.db.patch(node, { aggregate: newAggregate });
|
|
1667
1666
|
const deficientSubtree = await ctx.db.get(n.subtrees[i]);
|
|
1668
|
-
const minNodeSize =
|
|
1667
|
+
const minNodeSize = minNodeSizeFor(maxNodeSize);
|
|
1669
1668
|
if (deficientSubtree.items.length < minNodeSize) {
|
|
1670
1669
|
log(`deficient subtree ${deficientSubtree._id}`);
|
|
1671
1670
|
if (i > 0) {
|
|
@@ -1751,26 +1750,28 @@ async function mergeNodes(db, parent, leftIndex) {
|
|
|
1751
1750
|
});
|
|
1752
1751
|
await db.delete(right._id);
|
|
1753
1752
|
}
|
|
1754
|
-
async function negativeOffsetInNode(db, node, index, k1, k2) {
|
|
1755
|
-
const filtered = await filterBetween(db, node, k1, k2);
|
|
1753
|
+
async function negativeOffsetInNode(db, node, index, k1, k2, preloaded) {
|
|
1754
|
+
const filtered = await filterBetween(db, node, k1, k2, preloaded);
|
|
1756
1755
|
for (const included of filtered.reverse()) if (included.type === "item") {
|
|
1757
1756
|
if (index === 0) return included.item;
|
|
1758
1757
|
index -= 1;
|
|
1759
1758
|
} else {
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1759
|
+
const subtree = await db.get(included.subtree);
|
|
1760
|
+
const subtreeCount = (await nodeAggregate(db, subtree)).count;
|
|
1761
|
+
if (index < subtreeCount) return await negativeOffsetInNode(db, included.subtree, index, void 0, void 0, subtree);
|
|
1762
1762
|
index -= subtreeCount;
|
|
1763
1763
|
}
|
|
1764
1764
|
throw new ConvexError(`negative offset exceeded count by ${index} (in node ${node})`);
|
|
1765
1765
|
}
|
|
1766
|
-
async function atOffsetInNode(db, node, index, k1, k2) {
|
|
1767
|
-
const filtered = await filterBetween(db, node, k1, k2);
|
|
1766
|
+
async function atOffsetInNode(db, node, index, k1, k2, preloaded) {
|
|
1767
|
+
const filtered = await filterBetween(db, node, k1, k2, preloaded);
|
|
1768
1768
|
for (const included of filtered) if (included.type === "item") {
|
|
1769
1769
|
if (index === 0) return included.item;
|
|
1770
1770
|
index -= 1;
|
|
1771
1771
|
} else {
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1772
|
+
const subtree = await db.get(included.subtree);
|
|
1773
|
+
const subtreeCount = (await nodeAggregate(db, subtree)).count;
|
|
1774
|
+
if (index < subtreeCount) return await atOffsetInNode(db, included.subtree, index, void 0, void 0, subtree);
|
|
1774
1775
|
index -= subtreeCount;
|
|
1775
1776
|
}
|
|
1776
1777
|
throw new ConvexError(`offset exceeded count by ${index} (in node ${node})`);
|
|
@@ -1812,7 +1813,7 @@ function accumulate(nums) {
|
|
|
1812
1813
|
sum: 0
|
|
1813
1814
|
});
|
|
1814
1815
|
}
|
|
1815
|
-
async function insertIntoNode(ctx,
|
|
1816
|
+
async function insertIntoNode(ctx, maxNodeSize, node, item) {
|
|
1816
1817
|
const n = await ctx.db.get(node);
|
|
1817
1818
|
let i = 0;
|
|
1818
1819
|
for (; i < n.items.length; i++) {
|
|
@@ -1820,31 +1821,41 @@ async function insertIntoNode(ctx, namespace, node, item) {
|
|
|
1820
1821
|
if (compare === -1) break;
|
|
1821
1822
|
if (compare === 0) throw new ConvexError(`key ${p(item.k)} already exists in node ${n._id}`);
|
|
1822
1823
|
}
|
|
1824
|
+
let nextItems = n.items;
|
|
1825
|
+
let nextSubtrees = n.subtrees;
|
|
1823
1826
|
if (n.subtrees.length > 0) {
|
|
1824
|
-
const pushUp = await insertIntoNode(ctx,
|
|
1825
|
-
if (pushUp)
|
|
1826
|
-
|
|
1827
|
+
const pushUp = await insertIntoNode(ctx, maxNodeSize, n.subtrees[i], item);
|
|
1828
|
+
if (pushUp) {
|
|
1829
|
+
nextItems = [
|
|
1827
1830
|
...n.items.slice(0, i),
|
|
1828
1831
|
pushUp.item,
|
|
1829
1832
|
...n.items.slice(i)
|
|
1830
|
-
]
|
|
1831
|
-
|
|
1833
|
+
];
|
|
1834
|
+
nextSubtrees = [
|
|
1832
1835
|
...n.subtrees.slice(0, i),
|
|
1833
1836
|
pushUp.leftSubtree,
|
|
1834
1837
|
pushUp.rightSubtree,
|
|
1835
1838
|
...n.subtrees.slice(i + 1)
|
|
1836
|
-
]
|
|
1837
|
-
}
|
|
1838
|
-
} else
|
|
1839
|
+
];
|
|
1840
|
+
}
|
|
1841
|
+
} else nextItems = [
|
|
1839
1842
|
...n.items.slice(0, i),
|
|
1840
1843
|
item,
|
|
1841
1844
|
...n.items.slice(i)
|
|
1842
|
-
]
|
|
1845
|
+
];
|
|
1843
1846
|
const newAggregate = n.aggregate && add(n.aggregate, itemAggregate(item));
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1847
|
+
const nodePatch = {};
|
|
1848
|
+
if (nextItems !== n.items) nodePatch.items = nextItems;
|
|
1849
|
+
if (nextSubtrees !== n.subtrees) nodePatch.subtrees = nextSubtrees;
|
|
1850
|
+
if (newAggregate) nodePatch.aggregate = newAggregate;
|
|
1851
|
+
if (Object.keys(nodePatch).length > 0) await ctx.db.patch(node, nodePatch);
|
|
1852
|
+
const newN = {
|
|
1853
|
+
...n,
|
|
1854
|
+
items: nextItems,
|
|
1855
|
+
subtrees: nextSubtrees,
|
|
1856
|
+
aggregate: newAggregate ?? n.aggregate
|
|
1857
|
+
};
|
|
1858
|
+
const minNodeSize = minNodeSizeFor(maxNodeSize);
|
|
1848
1859
|
if (newN.items.length > maxNodeSize) {
|
|
1849
1860
|
if (newN.items.length !== maxNodeSize + 1 || newN.items.length !== 2 * minNodeSize + 1) throw new Error(`bad ${newN.items.length}`);
|
|
1850
1861
|
log(`splitting node ${newN._id} at ${newN.items[minNodeSize].k}`);
|
|
@@ -1889,6 +1900,7 @@ async function getOrCreateTree(db, namespace, maxNodeSize, rootLazy) {
|
|
|
1889
1900
|
const originalTree = await getTree(db, namespace);
|
|
1890
1901
|
const aggregateName = aggregateNameFromNamespace(namespace);
|
|
1891
1902
|
if (originalTree) {
|
|
1903
|
+
assertMaxNodeSize(originalTree.maxNodeSize);
|
|
1892
1904
|
if (originalTree.aggregateName !== aggregateName) {
|
|
1893
1905
|
await db.patch(originalTree._id, { aggregateName });
|
|
1894
1906
|
return {
|
|
@@ -1906,7 +1918,7 @@ async function getOrCreateTree(db, namespace, maxNodeSize, rootLazy) {
|
|
|
1906
1918
|
sum: 0
|
|
1907
1919
|
}
|
|
1908
1920
|
});
|
|
1909
|
-
const effectiveMaxNodeSize = maxNodeSize ?? await
|
|
1921
|
+
const effectiveMaxNodeSize = maxNodeSize ?? (await mustGetTree(db, void 0)).maxNodeSize;
|
|
1910
1922
|
const effectiveRootLazy = rootLazy ?? await isRootLazy(db, void 0) ?? true;
|
|
1911
1923
|
const id = await db.insert(AGGREGATE_TREE_TABLE, {
|
|
1912
1924
|
aggregateName,
|
|
@@ -1915,7 +1927,7 @@ async function getOrCreateTree(db, namespace, maxNodeSize, rootLazy) {
|
|
|
1915
1927
|
namespace
|
|
1916
1928
|
});
|
|
1917
1929
|
const newTree = await db.get(id);
|
|
1918
|
-
|
|
1930
|
+
assertMaxNodeSize(effectiveMaxNodeSize);
|
|
1919
1931
|
if (effectiveRootLazy) await db.patch(root, { aggregate: void 0 });
|
|
1920
1932
|
return newTree;
|
|
1921
1933
|
}
|
|
@@ -1930,6 +1942,19 @@ async function deleteTreeNodes(db, node) {
|
|
|
1930
1942
|
for (const subtree of current.subtrees) await deleteTreeNodes(db, subtree);
|
|
1931
1943
|
await db.delete(node);
|
|
1932
1944
|
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Deletes up to `limit` namespace trees belonging to one aggregate, returning
|
|
1947
|
+
* true once none are left. Unlike `clearTree` this does not recreate the tree,
|
|
1948
|
+
* so a caller can drain every namespace across several transactions.
|
|
1949
|
+
*/
|
|
1950
|
+
async function deleteTreesHandler(ctx, args) {
|
|
1951
|
+
const trees = await ctx.db.query(AGGREGATE_TREE_TABLE).withIndex("by_aggregate_name", (q) => q.eq("aggregateName", args.aggregateName)).take(args.limit);
|
|
1952
|
+
for (const tree of trees) {
|
|
1953
|
+
await deleteTreeNodes(ctx.db, tree.root);
|
|
1954
|
+
await ctx.db.delete(tree._id);
|
|
1955
|
+
}
|
|
1956
|
+
return trees.length === 0;
|
|
1957
|
+
}
|
|
1933
1958
|
async function clearTree(db, args) {
|
|
1934
1959
|
const tree = await getTree(db, args.namespace);
|
|
1935
1960
|
let existingRootLazy = true;
|
|
@@ -2319,6 +2344,17 @@ var Aggregate = class {
|
|
|
2319
2344
|
rootLazy: opts[0]?.rootLazy
|
|
2320
2345
|
});
|
|
2321
2346
|
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Deletes up to `limit` namespace trees without recreating them. Returns true
|
|
2349
|
+
* once this aggregate owns no trees, so callers can drain every namespace
|
|
2350
|
+
* across several mutations instead of walking them all in one.
|
|
2351
|
+
*/
|
|
2352
|
+
async deleteTrees(ctx, limit) {
|
|
2353
|
+
return deleteTreesHandler({ db: ctx.db }, {
|
|
2354
|
+
aggregateName: this.aggregateName,
|
|
2355
|
+
limit
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2322
2358
|
async makeRootLazy(ctx, namespace) {
|
|
2323
2359
|
const tree = await getOrCreateTree(ctx.db, namespaceForArg(this.aggregateName, { namespace }));
|
|
2324
2360
|
await ctx.db.patch(tree.root, { aggregate: void 0 });
|
package/dist/watcher.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as generateMeta, c as PARSE_SNAPSHOT_SUFFIX, i as resolveConfiguredBackend, n as withLocalCodegenEnv, o as getConvexConfig, r as loadCliConfig, s as logger } from "./local-env-
|
|
2
|
+
import { a as generateMeta, c as PARSE_SNAPSHOT_SUFFIX, i as resolveConfiguredBackend, n as withLocalCodegenEnv, o as getConvexConfig, r as loadCliConfig, s as logger } from "./local-env-yFKub75x.mjs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
@@ -4390,26 +4390,26 @@ declare const BUILTIN_SCHEMA_EXTENSIONS: readonly [SchemaExtension<{
|
|
|
4390
4390
|
fieldName: "status";
|
|
4391
4391
|
};
|
|
4392
4392
|
};
|
|
4393
|
-
|
|
4394
|
-
_: {
|
|
4395
|
-
notNull: true;
|
|
4396
|
-
};
|
|
4397
|
-
} & {
|
|
4393
|
+
cursor: ConvexTextBuilderInitial<""> & {
|
|
4398
4394
|
_: {
|
|
4399
4395
|
tableName: "aggregate_state";
|
|
4400
4396
|
};
|
|
4401
4397
|
} & {
|
|
4402
4398
|
_: {
|
|
4403
|
-
fieldName: "
|
|
4399
|
+
fieldName: "cursor";
|
|
4404
4400
|
};
|
|
4405
4401
|
};
|
|
4406
|
-
|
|
4402
|
+
kind: ConvexTextBuilderInitial<""> & {
|
|
4403
|
+
_: {
|
|
4404
|
+
notNull: true;
|
|
4405
|
+
};
|
|
4406
|
+
} & {
|
|
4407
4407
|
_: {
|
|
4408
4408
|
tableName: "aggregate_state";
|
|
4409
4409
|
};
|
|
4410
4410
|
} & {
|
|
4411
4411
|
_: {
|
|
4412
|
-
fieldName: "
|
|
4412
|
+
fieldName: "kind";
|
|
4413
4413
|
};
|
|
4414
4414
|
};
|
|
4415
4415
|
updatedAt: ConvexNumberBuilderInitial<""> & {
|
|
@@ -4526,6 +4526,7 @@ declare const BUILTIN_SCHEMA_EXTENSIONS: readonly [SchemaExtension<{
|
|
|
4526
4526
|
by_creation_time: ["_creationTime"];
|
|
4527
4527
|
by_kind_table_index: ["kind", "tableKey", "indexName", "_creationTime"];
|
|
4528
4528
|
by_kind_status: ["kind", "status", "_creationTime"];
|
|
4529
|
+
by_table_status: ["tableKey", "status", "_creationTime"];
|
|
4529
4530
|
}, {}, {}, {}>;
|
|
4530
4531
|
}>, SchemaExtension<{
|
|
4531
4532
|
readonly migration_state: ConvexTableWithColumns<{
|
package/package.json
CHANGED
|
@@ -54,7 +54,7 @@ Windowed count: `count({ where, orderBy, skip, take, cursor })` counts rows with
|
|
|
54
54
|
| Error | Cause |
|
|
55
55
|
| -------------------------- | -------------------------------------------- |
|
|
56
56
|
| `COUNT_NOT_INDEXED` | No `aggregateIndex` matches the filter shape |
|
|
57
|
-
| `COUNT_FILTER_UNSUPPORTED` |
|
|
57
|
+
| `COUNT_FILTER_UNSUPPORTED` | Unsupported operators, or a range count/aggregate over `aggregateWorkBudget` work units |
|
|
58
58
|
| `COUNT_INDEX_BUILDING` | Index still backfilling |
|
|
59
59
|
| `COUNT_RLS_UNSUPPORTED` | Called in RLS-restricted context |
|
|
60
60
|
|
|
@@ -310,6 +310,15 @@ If rank or aggregate state gets out of sync:
|
|
|
310
310
|
kitcn aggregate rebuild
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
+
Rebuild clears stored state in scheduled batches. Indexes report `CLEARING`
|
|
314
|
+
while draining and `BUILDING` while recomputing; wait for `READY`.
|
|
315
|
+
Writes targeting a declared index in `CLEARING` fail before the document write;
|
|
316
|
+
retry them after the index advances to `BUILDING` or `READY`.
|
|
317
|
+
Automatic pruning follows aggregate lifecycle state, so kickoff reads stay
|
|
318
|
+
bounded as the backing tables grow. For state-less storage, call
|
|
319
|
+
`aggregateBackfill` in `prune` mode with exact `tableName` and `indexName`
|
|
320
|
+
arguments.
|
|
321
|
+
|
|
313
322
|
## When to Use
|
|
314
323
|
|
|
315
324
|
| Need | Use |
|