[econ] some invention stats

This commit is contained in:
Devin Zuczek
2026-10-02 14:01:00 -04:00
parent bfe88a9837
commit fd2e266e50
4 changed files with 66 additions and 8 deletions
@@ -0,0 +1,20 @@
-- An invention's `NumDownloads` is the number of players who have it: the rows in
-- `inventory_invention` that name it. `grantInvention` now re-derives it on every grant
-- (packages/domain/src/inventory-invention-db.ts); every purchase before that left it at
-- the 0 a save mints, so this backfills the inventions already bought.
--
-- The `invention` table is the `api` worker's (same database). `ModifiedAt` is left alone:
-- being bought is not an edit of the invention.
-- Backs that per-invention count; the primary key leads with account_id.
-- Generated from INVENTORY_INVENTION_SCHEMA_DDL — keep in sync.
CREATE INDEX IF NOT EXISTS idx_inventory_invention_invention
ON inventory_invention (invention_id);
UPDATE invention
SET data = json_set(
data,
'$.NumDownloads',
(SELECT COUNT(*) FROM inventory_invention WHERE invention_id = invention.id)
)
WHERE id IN (SELECT invention_id FROM inventory_invention);
+4 -5
View File
@@ -2675,9 +2675,8 @@ interface SettledInventionPurchase {
*
* Ownership is recorded in `inventory_invention`; the creator is not sold their own
* invention (they own it already, via CreatorPlayerId) and a re-buy is a 409 rather
* than a second row. The invention’s `NumDownloads` counter is deliberately NOT
* bumped: that column lives on the `invention` table the `api` worker owns, and this
* worker only reads it.
* than a second row. The grant also resyncs the invention’s `NumDownloads` — the count
* of players who have it — and the invention returned carries the new number.
*/
async function settleInventionPurchase(
c: Context<App>,
@@ -2723,7 +2722,7 @@ async function settleInventionPurchase(
// around them, so order them by what a failure costs. A buyer who paid and got the
// invention but left the creator unpaid is recoverable; a buyer charged for nothing
// is not.
await grantInvention(c.env.DB, id, inventionId)
const downloads = await grantInvention(c.env.DB, id, inventionId)
if (price > 0) {
// Seed the creator's signup grant BEFORE crediting them: `creditCurrency` upserts
@@ -2760,7 +2759,7 @@ async function settleInventionPurchase(
if (price > 0) {
await pushBalancePurchase(c, id, CurrencyType.RecCenterTokens, -price, balance)
}
return { invention, balance }
return { invention: { ...invention, NumDownloads: downloads }, balance }
}
// strict: false so trailing-slash routes (e.g. `/gifts/consume/`, which the client
+15 -2
View File
@@ -5054,7 +5054,7 @@ describe('econ endpoints', () => {
}
InventionResponse: {
Status: number
Invention: { InventionId: number; Name: string }
Invention: { InventionId: number; Name: string; NumDownloads: number }
InventionVersion: { InventionId: number; VersionNumber: number }
}
}
@@ -5070,9 +5070,22 @@ describe('econ endpoints', () => {
expect(await getOwnedInventionIds(env.DB, 50)).toEqual([8])
// Owning an invention is boolean: buying it again is a conflict, not a second row.
// `NumDownloads` is the number of players who have it, re-derived on the grant:
// the response carries it and so does the stored record.
const storedDownloads = async (): Promise<number> =>
(
await env.DB.prepare(
"SELECT json_extract(data, '$.NumDownloads') AS n FROM invention WHERE id = 8"
).first<{ n: number }>()
)?.n ?? -1
expect(body.InventionResponse.Invention.NumDownloads).toBe(1)
expect(await storedDownloads()).toBe(1)
// Owning an invention is boolean: buying it again is a conflict, not a second row —
// and not a second download.
expect((await buyInvention('50', 8)).status).toBe(409)
expect(await getOwnedInventionIds(env.DB, 50)).toEqual([8])
expect(await storedDownloads()).toBe(1)
})
test('GET /api/storefronts/v2/buyInvention pays the creator the buyer’s tokens', async () => {