nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: mlange-42/arche
Pull request #253 (2023-05-01 22:41:39)

Optimization: Cached filters use swap-remove when removing an archetype

coveralls: ## Pull Request Test Coverage Report for [Build 4855612657](https://coveralls.io/builds/59503262)

* **4** of **4**   **(100.0%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/59503262/badge)](https://coveralls.io/builds/59503262) |
| :-- | --: |
| Change from base [Build 4855602543](https://coveralls.io/builds/59503218): |  0.0% |
| Covered Lines: | 3412 |
| Relevant Lines: | 3412 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d9642a..912c06e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@
 * Relation archetypes are recycled in archetype graph nodes (#248)
 * Already empty archetypes are not zeroed on reset (#248)
 * Optimize `RelationFilter`: get archetype directly instead of iterating complete node (#251)
+* Cached filters use swap-remove when removing an archetype (#253)
 
 ### Other
 
diff --git a/ecs/archetypes.go b/ecs/archetypes.go
index 02dbad6..f17b98c 100644
--- a/ecs/archetypes.go
+++ b/ecs/archetypes.go
@@ -51,10 +51,12 @@ func (a *archetypePointers) Add(arch *archetype) {
 
 // Remove an element.
 func (a *archetypePointers) Remove(arch *archetype) {
-	for i := 0; i < len(a.pointers); i++ {
+	ln := len(a.pointers)
+	for i := 0; i < ln; i++ {
 		arch2 := a.pointers[i]
 		if arch == arch2 {
-			a.pointers = append(a.pointers[:i], a.pointers[i+1:]...)
+			a.pointers[i], a.pointers[ln-1] = a.pointers[ln-1], nil
+			a.pointers = a.pointers[:ln-1]
 			return
 		}
 	}


Test patch:
diff --git a/ecs/archetypes_test.go b/ecs/archetypes_test.go
index b87b466..84babb2 100644
--- a/ecs/archetypes_test.go
+++ b/ecs/archetypes_test.go
@@ -20,17 +20,20 @@ func TestArchetypePointers(t *testing.T) {
 
 	assert.Equal(t, int32(3), pt.Len())
 
-	for i := 0; i < 45; i++ {
-		pt.Add(&a3)
+	var last archetype
+	for i := 0; i < 15; i++ {
+		last = archetype{}
+		pt.Add(&last)
 	}
 
 	assert.Equal(t, unsafe.Pointer(&a1), unsafe.Pointer(pt.Get(0)))
 	assert.Equal(t, unsafe.Pointer(&a2), unsafe.Pointer(pt.Get(1)))
 	assert.Equal(t, unsafe.Pointer(&a3), unsafe.Pointer(pt.Get(2)))
 
-	assert.Equal(t, int32(48), pt.Len())
+	assert.Equal(t, int32(18), pt.Len())
 
 	pt.Remove(&a2)
-	assert.Equal(t, int32(47), pt.Len())
-	assert.Equal(t, unsafe.Pointer(&a3), unsafe.Pointer(pt.Get(1)))
+	assert.Equal(t, int32(17), pt.Len())
+	assert.Equal(t, unsafe.Pointer(&last), unsafe.Pointer(pt.Get(1)))
+	assert.Equal(t, unsafe.Pointer(&a3), unsafe.Pointer(pt.Get(2)))
 }
Repository: ponder-sh/ponder
Pull request #1555 (2025-02-24 21:55:33)

fix: flush index
Fixed a regression where cached indexing data wouldn't get evicted properly when `totalCacheOps < num cache entries`. This would only occur when all or a high percentage of cache operations were inserts and not updates.


Patch:
diff --git a/.changeset/itchy-chairs-share.md b/.changeset/itchy-chairs-share.md
new file mode 100644
index 00000000..290cf710
--- /dev/null
+++ b/.changeset/itchy-chairs-share.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed a regression with evicting cached indexing data introduced in v0.9.20.
diff --git a/packages/core/src/indexing-store/cache.ts b/packages/core/src/indexing-store/cache.ts
index 2831e481..ef110e6f 100644
--- a/packages/core/src/indexing-store/cache.ts
+++ b/packages/core/src/indexing-store/cache.ts
@@ -594,6 +594,7 @@ export const createIndexingCache = ({
           for (const [key, entry] of insertBuffer.get(table)!) {
             const bytes = getBytes(entry.row);
             cacheBytes += bytes;
+            totalCacheOps++;
             tableSpillover.set(key, {
               bytes,
               operationIndex: 0,
@@ -752,7 +753,7 @@ export const createIndexingCache = ({
 
         for (const tableCache of cache.values()) {
           for (const [key, entry] of tableCache) {
-            if (entry.operationIndex < flushIndex) {
+            if (entry.operationIndex <= flushIndex) {
               tableCache.delete(key);
               cacheBytes -= entry.bytes;
               rowCount += 1;


Test patch:
diff --git a/packages/core/src/indexing-store/cache.test.ts b/packages/core/src/indexing-store/cache.test.ts
index 95a351c7..fdeded37 100644
--- a/packages/core/src/indexing-store/cache.test.ts
+++ b/packages/core/src/indexing-store/cache.test.ts
@@ -247,3 +247,49 @@ test("flush() encoding escape", async (context) => {
     expect(result).toStrictEqual(values);
   });
 });
+
+test("commit() evicts rows", async (context) => {
+  const schema = {
+    account: onchainTable("account", (p) => ({
+      address: p.hex().primaryKey(),
+      balance: p.bigint().notNull(),
+    })),
+  };
+
+  const { database } = await setupDatabaseServices(context, {
+    schemaBuild: { schema },
+  });
+
+  const indexingCache = createIndexingCache({
+    common: context.common,
+    schemaBuild: { schema },
+    checkpoint: ZERO_CHECKPOINT_STRING,
+  });
+
+  context.common.options.indexingCacheMaxBytes = 0;
+
+  await database.transaction(async (client, tx) => {
+    const indexingStore = createHistoricalIndexingStore({
+      common: context.common,
+      schemaBuild: { schema },
+      indexingCache,
+      db: tx,
+      client,
+    });
+
+    await indexingStore.insert(schema.account).values({
+      address: zeroAddress,
+      balance: 10n,
+    });
+
+    await indexingCache.flush({ client });
+    indexingCache.commit();
+
+    const result = indexingCache.has({
+      table: schema.account,
+      key: { address: zeroAddress },
+    });
+
+    expect(result).toBe(false);
+  });
+});
Repository: ponder-sh/ponder
Pull request #1557 (2025-02-25 04:14:50)

fix `flush()` when using raw sql
Fixes a bug that occurs when calling `flush()` multiple times for the same batch of events (same transaction). This occurs because the "update" logic creates a temp table and isn't accounting for the case when the temp table already exists.

With this pr, the temp table is only created if not exists and truncated at the end of the function. Another solution would be to drop the temp table at the end of the function and recreate it on every call.


Patch:
diff --git a/.changeset/few-trains-rule.md b/.changeset/few-trains-rule.md
new file mode 100644
index 00000000..306b2e11
--- /dev/null
+++ b/.changeset/few-trains-rule.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed a regression introduced in v0.9.20 that caused an error when using raw sql queries.
diff --git a/packages/core/src/indexing-store/cache.ts b/packages/core/src/indexing-store/cache.ts
index ef110e6f..55dbc275 100644
--- a/packages/core/src/indexing-store/cache.ts
+++ b/packages/core/src/indexing-store/cache.ts
@@ -629,7 +629,7 @@ export const createIndexingCache = ({
             .join(",\n");
 
           const createTempTableQuery = `
-              CREATE TEMP TABLE "${getTableName(table)}" 
+              CREATE TEMP TABLE IF NOT EXISTS "${getTableName(table)}" 
               ON COMMIT DROP
               AS SELECT * FROM "${
                 getTableConfig(table).schema ?? "public"
@@ -646,6 +646,7 @@ export const createIndexingCache = ({
                 .map(({ sql }) => `target."${sql}" = source."${sql}"`)
                 .join(" AND ")};
             `;
+          const truncateQuery = `TRUNCATE TABLE "${getTableName(table)}" CASCADE`;
 
           const text = getCopyText(
             table,
@@ -695,6 +696,8 @@ export const createIndexingCache = ({
             });
             // @ts-ignore
             await client.query(updateQuery);
+            // @ts-ignore
+            await client.query(truncateQuery);
           } finally {
             common.metrics.ponder_indexing_cache_query_duration.observe(
               {


Test patch:
diff --git a/packages/core/src/indexing-store/cache.test.ts b/packages/core/src/indexing-store/cache.test.ts
index fdeded37..4d8c1378 100644
--- a/packages/core/src/indexing-store/cache.test.ts
+++ b/packages/core/src/indexing-store/cache.test.ts
@@ -110,7 +110,24 @@ test("flush() update", async (context) => {
 
     await indexingCache.flush({ client });
 
-    const result = await indexingStore.find(schema.account, {
+    let result = await indexingStore.find(schema.account, {
+      address: zeroAddress,
+    });
+
+    expect(result).toStrictEqual({
+      address: zeroAddress,
+      balance: 12n,
+    });
+
+    // flush again to make sure temp tables are cleaned up
+
+    await indexingStore.update(schema.account, { address: zeroAddress }).set({
+      balance: 12n,
+    });
+
+    await indexingCache.flush({ client });
+
+    result = await indexingStore.find(schema.account, {
       address: zeroAddress,
     });
Repository: ponder-sh/ponder
Pull request #1619 (2025-03-25 16:15:41)

fix `client.getStatus()`


Patch:
diff --git a/.changeset/chilly-hornets-thank.md b/.changeset/chilly-hornets-thank.md
new file mode 100644
index 00000000..d53e4b05
--- /dev/null
+++ b/.changeset/chilly-hornets-thank.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed a bug with `client.getStatus()`.
diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts
index 827926f9..d7f18ac2 100644
--- a/packages/core/src/client/index.ts
+++ b/packages/core/src/client/index.ts
@@ -2,19 +2,12 @@ import type { Schema } from "@/internal/types.js";
 import type { ReadonlyDrizzle } from "@/types/db.js";
 import { promiseWithResolvers } from "@/utils/promiseWithResolvers.js";
 import type { QueryWithTypings } from "drizzle-orm";
-import { type PgSession, pgTable } from "drizzle-orm/pg-core";
+import type { PgSession } from "drizzle-orm/pg-core";
 import { createMiddleware } from "hono/factory";
 import { streamSSE } from "hono/streaming";
 import superjson from "superjson";
 import { validateQuery } from "./validate.js";
 
-const status = pgTable("_ponder_status", (t) => ({
-  chainId: t.bigint({ mode: "number" }).primaryKey(),
-  blockNumber: t.bigint({ mode: "number" }),
-  blockTimestamp: t.bigint({ mode: "number" }),
-  ready: t.boolean().notNull(),
-}));
-
 /**
  * Middleware for `@ponder/client`.
  *
@@ -129,7 +122,7 @@ export const client = ({
     }
 
     if (c.req.path === "/sql/status") {
-      const statusResult = await db.select().from(status);
+      const statusResult = await globalThis.PONDER_DATABASE.getStatus();
       return c.json(statusResult);
     }
 


Test patch:
diff --git a/packages/core/src/client/index.test.ts b/packages/core/src/client/index.test.ts
index c1bd3e52..6eae3840 100644
--- a/packages/core/src/client/index.test.ts
+++ b/packages/core/src/client/index.test.ts
@@ -177,3 +177,23 @@ FROM infinite_cte;`,
   expect(response.status).toBe(500);
   expect(await response.text()).toContain("Recursive CTEs not supported");
 });
+
+test("client.status", async (context) => {
+  globalThis.PONDER_NAMESPACE_BUILD = "public";
+
+  const { database } = await setupDatabaseServices(context);
+
+  globalThis.PONDER_DATABASE = database;
+
+  const app = new Hono().use(
+    client({
+      db: database.qb.drizzleReadonly,
+      schema: {},
+    }),
+  );
+
+  const response = await app.request("/sql/status");
+  expect(response.status).toBe(200);
+  const result = await response.json();
+  expect(result).toMatchInlineSnapshot("null");
+});
Repository: ponder-sh/ponder
Pull request #1672 (2025-04-15 14:55:14)

fix `insert().onConflictDoNothing()`


Patch:
diff --git a/.changeset/light-snakes-yell.md b/.changeset/light-snakes-yell.md
new file mode 100644
index 00000000..f86c68a2
--- /dev/null
+++ b/.changeset/light-snakes-yell.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed a bug with `insert().onConflictDoNothing()` that would cause an error "Cannot read properties of undefined".
diff --git a/packages/core/src/indexing-store/realtime.ts b/packages/core/src/indexing-store/realtime.ts
index 12a4169f..002c2490 100644
--- a/packages/core/src/indexing-store/realtime.ts
+++ b/packages/core/src/indexing-store/realtime.ts
@@ -61,6 +61,10 @@ export const createRealtimeIndexingStore = ({
                     return result.length === 1 ? result[0] : null;
                   }
 
+                  if (result.length === 0) {
+                    return new Array(values.length).fill(null);
+                  }
+
                   const rows = [];
                   let resultIndex = 0;
 


Test patch:
diff --git a/packages/core/src/indexing-store/realtime.test.ts b/packages/core/src/indexing-store/realtime.test.ts
index a08e1ac3..c4ea873c 100644
--- a/packages/core/src/indexing-store/realtime.test.ts
+++ b/packages/core/src/indexing-store/realtime.test.ts
@@ -155,6 +155,16 @@ test("insert", async (context) => {
     { address: "0x0000000000000000000000000000000000000003", balance: 0n },
   ]);
 
+  result = await indexingStore
+    .insert(schema.account)
+    .values([
+      { address: "0x0000000000000000000000000000000000000001", balance: 44n },
+      { address: "0x0000000000000000000000000000000000000001", balance: 44n },
+    ])
+    .onConflictDoNothing();
+
+  expect(result).toStrictEqual([null, null]);
+
   result = await indexingStore.find(schema.account, {
     address: "0x0000000000000000000000000000000000000001",
   });
Repository: speee/jsx-slack
Pull request #69 (2019-10-10 05:42:47)

Fix invalid array children in template literal
By the similar reason as #53, `jsxslack` template literal tag cannot place array as children. It means that `<Option>` embedded list won't work in `<Select>`'s children unlike JSX.

```
// TypeError: Cannot read property 'map' of undefined
jsxslack`
  <Blocks>
    <Actions>
      <Select>
        ${[...Array(10)].map(
          (_, i) =>
            jsxslack.fragment`<Option value=${i.toString()}>${i}</Option>`
        )}
      </Select>
    </Actions>
  </Blocks>
`
```

I've fixed this by using `lodash.flattendeep` in first parsing of template literal.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ec1c1e..03d2e0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [Unreleased]
 
+### Fixed
+
+- Fix invalid array children in template literal ([#69](https://github.com/speee/jsx-slack/pull/69))
+
 ## v0.10.0 - 2019-10-02
 
 ### Added
diff --git a/src/tag.ts b/src/tag.ts
index f7483f8..4bb5640 100644
--- a/src/tag.ts
+++ b/src/tag.ts
@@ -1,6 +1,7 @@
 /* eslint-disable no-new-wrappers */
 import he from 'he'
 import htm from 'htm'
+import flattenDeep from 'lodash.flattendeep'
 import * as blockKitComponents from './components'
 import * as dialogComponents from './dialog/components'
 import { JSXSlack } from './index'
@@ -42,7 +43,7 @@ const firstPass: JSXSlackTemplate<VirtualNode | VirtualNode[]> = htm.bind(
           {}
         )
       : props,
-    children: children.map(c => normalize(c)),
+    children: flattenDeep(children.map(c => normalize(c))),
   })
 )
 


Test patch:
diff --git a/test/tag.tsx b/test/tag.tsx
index 05acd5d..5c02c8b 100644
--- a/test/tag.tsx
+++ b/test/tag.tsx
@@ -116,6 +116,35 @@ describe('Tagged template', () => {
     `).toMatchSnapshot()
   })
 
+  it('can use fragmented options in <Select>', () => {
+    const template = jsxslack`
+      <Blocks>
+        <Actions>
+          <Select>
+            ${[...Array(10)].map(
+              (_, i) =>
+                jsxslack.fragment`<Option value=${i.toString()}>${i}</Option>`
+            )}
+          </Select>
+        </Actions>
+      </Blocks>
+    `
+
+    expect(template).toStrictEqual(
+      JSXSlack(
+        <Blocks>
+          <Actions>
+            <BlockKitSelect>
+              {[...Array(10)].map((_, i) => (
+                <BlockKitOption value={i.toString()}>{i}</BlockKitOption>
+              ))}
+            </BlockKitSelect>
+          </Actions>
+        </Blocks>
+      )
+    )
+  })
+
   it('has same decode behavior compatible with JSX for HTML entities', () => {
     const [jsxEntitySection] = JSXSlack(
       <Blocks>
Repository: mlange-42/arche
Pull request #317 (2024-01-11 00:11:39)

Entity generation changed from unit16 to uint32
It uses the space anyway due to padding...

coveralls: ## Pull Request Test Coverage Report for [Build 7482348518](https://coveralls.io/builds/65003528)


* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/65003528/badge)](https://coveralls.io/builds/65003528) |
| :-- | --: |
| Change from base [Build 7479245218](https://coveralls.io/builds/64996335): |  0.0% |
| Covered Lines: | 3852 |
| Relevant Lines: | 3852 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac7af66..071d2ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,10 @@ This change was necessary to get the same performance as before, despite the mor
 * Adds functions `ComponentType(*World, ID) reflect.Type` and `ResourceType(*World, ID) reflect.Type` (#315)
 * Adds methods `World.Ids(Entity) []ID` and `Query.Ids() []ID` (#315)
 
+### Other
+
+* Entity generation data type changed from `uint16` to `uint32` (#317)
+
 ## [[v0.9.0]](https://github.com/mlange-42/arche/compare/v0.8.1...v0.9.0)
 
 ### Infrastructure
diff --git a/ecs/entity.go b/ecs/entity.go
index c294e6b..fff9586 100644
--- a/ecs/entity.go
+++ b/ecs/entity.go
@@ -22,7 +22,7 @@ var entityIndexSize uint32 = uint32(reflect.TypeOf(entityIndex{}).Size())
 // The zero value should be used to indicate "nil", and can be checked with [Entity.IsZero].
 type Entity struct {
 	id  eid    // Entity ID
-	gen uint16 // Entity generation
+	gen uint32 // Entity generation
 }
 
 // newEntity creates a new Entity.
@@ -31,7 +31,7 @@ func newEntity(id eid) Entity {
 }
 
 // newEntityGen creates a new Entity with a given generation.
-func newEntityGen(id eid, gen uint16) Entity {
+func newEntityGen(id eid, gen uint32) Entity {
 	return Entity{id, gen}
 }
 
diff --git a/ecs/pool.go b/ecs/pool.go
index 802e436..e3c9e20 100644
--- a/ecs/pool.go
+++ b/ecs/pool.go
@@ -20,7 +20,7 @@ type entityPool struct {
 // newEntityPool creates a new, initialized Entity pool.
 func newEntityPool(capacityIncrement uint32) entityPool {
 	entities := make([]Entity, 1, capacityIncrement)
-	entities[0] = Entity{0, math.MaxUint16}
+	entities[0] = Entity{0, math.MaxUint32}
 	return entityPool{
 		entities:          entities,
 		next:              0,


Test patch:
diff --git a/ecs/pool_test.go b/ecs/pool_test.go
index 60e4faa..e82d658 100644
--- a/ecs/pool_test.go
+++ b/ecs/pool_test.go
@@ -16,7 +16,7 @@ func TestEntityPool(t *testing.T) {
 	p := newEntityPool(128)
 
 	expectedAll := []Entity{newEntity(0), newEntity(1), newEntity(2), newEntity(3), newEntity(4), newEntity(5)}
-	expectedAll[0].gen = math.MaxUint16
+	expectedAll[0].gen = math.MaxUint32
 
 	for i := 0; i < 5; i++ {
 		_ = p.Get()
Repository: mlange-42/arche
Pull request #408 (2024-03-02 22:38:26)

Add entity read access
When integrating with external systems (network in this case).  Need a read only way to check Entities ID and equality.

coveralls: ## Pull Request Test Coverage Report for [Build 8125885644](https://coveralls.io/builds/66043030)


### Details

* **9** of **9**   **(100.0%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/66043030/badge)](https://coveralls.io/builds/66043030) |
| :-- | --: |
| Change from base [Build 7996927484](https://coveralls.io/builds/65842553): |  0.0% |
| Covered Lines: | 6375 |
| Relevant Lines: | 6375 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/ecs/entity.go b/ecs/entity.go
index 4dbfadd..582a2ab 100644
--- a/ecs/entity.go
+++ b/ecs/entity.go
@@ -43,6 +43,16 @@ func (e Entity) IsZero() bool {
 	return e.id == 0
 }
 
+// ID returns the entity's ID.
+func (e Entity) ID() uint32 {
+	return uint32(e.id)
+}
+
+// Generation returns the entity's generation.
+func (e Entity) Generation() uint32 {
+	return e.gen
+}
+
 // MarshalJSON returns a JSON representation of the entity, for serialization purposes.
 //
 // The JSON representation of an entity is a two-element array of entity ID and generation.


Test patch:
diff --git a/ecs/entity_test.go b/ecs/entity_test.go
index 7eaf592..bfe0f6f 100644
--- a/ecs/entity_test.go
+++ b/ecs/entity_test.go
@@ -16,6 +16,18 @@ func TestEntityAsIndex(t *testing.T) {
 	_ = val
 }
 
+func TestEntityID(t *testing.T) {
+	e := newEntityGen(1, 2)
+
+	assert.Equal(t, e.ID(), uint32(1))
+}
+
+func TestEntityGeneration(t *testing.T) {
+	e := newEntityGen(1, 2)
+
+	assert.Equal(t, e.Generation(), uint32(2))
+}
+
 func TestZeroEntity(t *testing.T) {
 	assert.True(t, Entity{}.IsZero())
 	assert.False(t, Entity{1, 0}.IsZero())
Repository: mlange-42/arche
Pull request #420 (2024-05-16 10:42:22)

Add function ResourceTypeID to get resource ID from reflect.Type

coveralls: ## Pull Request Test Coverage Report for [Build 9110878622](https://coveralls.io/builds/67527098)


### Details

* **3** of **3**   **(100.0%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/67527098/badge)](https://coveralls.io/builds/67527098) |
| :-- | --: |
| Change from base [Build 9053955949](https://coveralls.io/builds/67440853): |  0.0% |
| Covered Lines: | 6380 |
| Relevant Lines: | 6380 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0d3b4f..e1493bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## [[unpublished]](https://github.com/mlange-42/arche/compare/v0.12.0...main)
 
+### Features
+
+* Adds function `ResourceTypeID` to register/get a resource ID from a `reflect.Type` (#420)
+
 ### Other
 
 * Fix component type in examples/base (#419)
diff --git a/ecs/functions.go b/ecs/functions.go
index eb9aa63..622e1ed 100644
--- a/ecs/functions.go
+++ b/ecs/functions.go
@@ -60,6 +60,16 @@ func ResourceID[T any](w *World) ResID {
 	return w.resourceID(tp)
 }
 
+// ResourceTypeID returns the [ResID] for a resource type.
+// Registers the type if it is not already registered.
+//
+// See [ResourceID] for a more commonly used generic variant.
+//
+// The number of resources per [World] is limited to [MaskTotalBits].
+func ResourceTypeID(w *World, tp reflect.Type) ResID {
+	return w.resourceID(tp)
+}
+
 // ResourceIDs returns a list of all registered resource IDs.
 func ResourceIDs(w *World) []ResID {
 	intIds := w.resources.registry.IDs


Test patch:
diff --git a/ecs/functions_test.go b/ecs/functions_test.go
index bacc614..2022dea 100644
--- a/ecs/functions_test.go
+++ b/ecs/functions_test.go
@@ -13,14 +13,19 @@ func TestCompResIDs(t *testing.T) {
 	posID := ComponentID[Position](&w)
 	rotID := ComponentID[rotation](&w)
 
+	tPosID := TypeID(&w, reflect.TypeOf(Position{}))
+	tRotID := TypeID(&w, reflect.TypeOf(rotation{}))
+
 	res1ID := ResourceID[Position](&w)
 	res2ID := ResourceID[Velocity](&w)
 
-	tPosID := TypeID(&w, reflect.TypeOf(Position{}))
-	tRotID := TypeID(&w, reflect.TypeOf(rotation{}))
+	tRes1ID := ResourceTypeID(&w, reflect.TypeOf(Position{}))
+	tRes2ID := ResourceTypeID(&w, reflect.TypeOf(Velocity{}))
 
 	assert.Equal(t, posID, tPosID)
 	assert.Equal(t, rotID, tRotID)
+	assert.Equal(t, res1ID, tRes1ID)
+	assert.Equal(t, res2ID, tRes2ID)
 
 	assert.Equal(t, uint8(0), posID.id)
 	assert.Equal(t, uint8(1), rotID.id)