<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: hiro@</title>
    <description>The latest articles on DEV Community by hiro@ (@hirodeath).</description>
    <link>https://dev.to/hirodeath</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F57464%2F7deedba2-079f-4a50-91e6-065b4e20a0c0.jpeg</url>
      <title>DEV Community: hiro@</title>
      <link>https://dev.to/hirodeath</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hirodeath"/>
    <language>en</language>
    <item>
      <title>A multi-device push rollout remains incomplete while old logout behavior survives</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Sat, 17 Oct 2026 01:48:27 +0000</pubDate>
      <link>https://dev.to/hirodeath/a-multi-device-push-rollout-remains-incomplete-while-old-logout-behavior-survives-3881</link>
      <guid>https://dev.to/hirodeath/a-multi-device-push-rollout-remains-incomplete-while-old-logout-behavior-survives-3881</guid>
      <description>&lt;p&gt;A server can store multiple push tokens per user while an old application's logout still removes every device registration. The new API behavior alone does not describe the state of a rollout.&lt;/p&gt;

&lt;p&gt;This investigation combined SquadNote's old and new APIs with old and new logout methods. Its focus is the combinations that can remain in use and the evidence needed to call the migration complete, rather than the registration and deletion implementation itself.&lt;/p&gt;

&lt;p&gt;The source versions are &lt;code&gt;circle-hub&lt;/code&gt; at &lt;code&gt;0cda1e8&lt;/code&gt; and the parent of change &lt;code&gt;ad0a669&lt;/code&gt;. Tests ran locally on September 11, 2026. No OTA update or notification to a physical device was sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  A migration can remain even without a new table
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/0cda1e865ad80d1529197729d8d436e96d56edc7/docs/multi-device-push.md" rel="noopener noreferrer"&gt;implementation specification&lt;/a&gt; says the existing token table and unique-token constraint suffice without a database migration.&lt;/p&gt;

&lt;p&gt;Before the change, registering a previously unregistered token also removed existing tokens for that user. Registering iOS and then Android left only the later registration.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/0cda1e865ad80d1529197729d8d436e96d56edc7/apps/web/src/server/api/routers/notification.ts" rel="noopener noreferrer"&gt;updated API&lt;/a&gt; upserts by token and preserves another device's registration. However, it retains &lt;code&gt;unregisterAllPushTokens&lt;/code&gt; for compatibility.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/circle-hub/commit/ad0a669" rel="noopener noreferrer"&gt;mobile change&lt;/a&gt; must also reach clients for ordinary logout to remove only that installation's tokens. Having an adequate server schema and having every caller use the new deletion scope are different conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combine both server versions with both deletion methods
&lt;/h2&gt;

&lt;p&gt;In disposable copies, the actual before-and-after tRPC routers were connected to the same kind of local libSQL database. An authenticated synthetic user registered an iOS value followed by an Android value. These were test strings, not actual Expo Push Tokens.&lt;/p&gt;

&lt;p&gt;The comparison then selected either the old caller's all-device deletion or the new caller's device-specific deletion. It exercised the API calls rather than running the application UI and network end to end.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Deletion method&lt;/th&gt;
&lt;th&gt;After 2 registrations&lt;/th&gt;
&lt;th&gt;After Android-side logout&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before change&lt;/td&gt;
&lt;td&gt;Delete all&lt;/td&gt;
&lt;td&gt;Android only&lt;/td&gt;
&lt;td&gt;0 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before change&lt;/td&gt;
&lt;td&gt;Delete device&lt;/td&gt;
&lt;td&gt;Android only&lt;/td&gt;
&lt;td&gt;0 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After change&lt;/td&gt;
&lt;td&gt;Delete all&lt;/td&gt;
&lt;td&gt;iOS and Android&lt;/td&gt;
&lt;td&gt;0 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After change&lt;/td&gt;
&lt;td&gt;Delete device&lt;/td&gt;
&lt;td&gt;iOS and Android&lt;/td&gt;
&lt;td&gt;iOS only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All four cases matched their assertions. Adopting only the new logout method cannot make the old server retain both destinations. Conversely, the new server still removes both if all-device deletion is called.&lt;/p&gt;

&lt;p&gt;The “old server + device deletion” case reaches 0 because registering Android already removed iOS. Device-specific deletion did not itself remove the iOS row. Inspect both the post-registration and post-logout states instead of only the final count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distribution does not restore deleted registrations
&lt;/h2&gt;

&lt;p&gt;The test also registered iOS again after logout and confirmed that its row existed. That was the result of another registration call with the required input. Updating the server did not automatically reconstruct deleted rows.&lt;/p&gt;

&lt;p&gt;A rollout therefore needs to distinguish distributing new code, applying it, and executing registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Distribute the new code
  ↓
The target device downloads and applies it
  ↓
The device executes registration
  ↓
The required destinations exist on the server
  ↓
Verify logout preserves the other device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a proposed verification sequence. The investigation did not collect the rollout stage of actual user devices. It does not establish that everyone updated or that all registrations were restored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching runtimeVersion labels are insufficient evidence
&lt;/h2&gt;

&lt;p&gt;The specification calls for updating Web/API first and then delivering compatible changes to both iOS and Android. It also records different native dependency configurations in the targeted builds for the two platforms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.expo.dev/eas-update/runtime-versions/" rel="noopener noreferrer"&gt;Expo's runtime-version documentation&lt;/a&gt; requires updates to be compatible with the native code embedded in a build. Assigning the same identifier manually does not make different native configurations identical.&lt;/p&gt;

&lt;p&gt;A rollout record can connect the API commit, each platform's build and runtimeVersion, the applied update, and the registration outcome. This article did not change delivery settings or infer successful distribution from local tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated checks and the remaining receipt checks
&lt;/h2&gt;

&lt;p&gt;The 4 additional cases passed alongside 8 existing registration, deletion, and sending tests, for 12 successful tests. The environment was Node.js &lt;code&gt;v24.15.0&lt;/code&gt; and Vitest &lt;code&gt;4.1.4&lt;/code&gt;. Tests used in-memory libSQL and the actual router code. External HTTP for notification sending was mocked.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/batch05-circle-tests.py" rel="noopener noreferrer"&gt;reproduction runner&lt;/a&gt; and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/push-rollout.test.ts" rel="noopener noreferrer"&gt;compatibility matrix test&lt;/a&gt; are recorded.&lt;/p&gt;

&lt;p&gt;These checks establish how the specified calls change database registrations. They do not cover physical-device permissions, delivery after Expo accepts a message, OS notification presentation, or recovery from offline operation.&lt;/p&gt;

&lt;p&gt;The original specification's physical-device checklist is also unchecked at the inspected revision. It calls for applying updates to both platforms, receiving on both devices, and receiving only on the remaining device after one logs out. A documented procedure is not evidence that it was executed.&lt;/p&gt;

&lt;p&gt;A completion record should include the server test results, remaining callers, update application, recreated registrations, and actual receipt checks.&lt;/p&gt;

</description>
      <category>expo</category>
      <category>mobile</category>
      <category>testing</category>
      <category>migration</category>
    </item>
    <item>
      <title>Consolidating Codex TODOs in Plane without merging different deliverables</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Fri, 16 Oct 2026 01:29:31 +0000</pubDate>
      <link>https://dev.to/hirodeath/consolidating-codex-todos-in-plane-without-merging-different-deliverables-18ca</link>
      <guid>https://dev.to/hirodeath/consolidating-codex-todos-in-plane-without-merging-different-deliverables-18ca</guid>
      <description>&lt;p&gt;Suppose a TODO to explain joining a group through an invitation link appears in more than one conversation. Merging it into an existing ticket because the title sounds familiar could combine a carousel and a video into one task.&lt;/p&gt;

&lt;p&gt;Consolidation works better when it identifies the target and deliverable. The same theme can require separate outputs. If a conversation continues existing work, decide where its new assumptions and remaining tasks belong.&lt;/p&gt;

&lt;p&gt;This record compares SquadNote's Plane tickets with fixed Git documents on September 11, 2026. Some descriptions retain a Codex chat date. It is not an experiment in automatically extracting tasks from complete conversations, and no tickets were created or merged during the audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate matching themes by their deliverables
&lt;/h2&gt;

&lt;p&gt;The production records include parent &lt;code&gt;SQN-46&lt;/code&gt; and children &lt;code&gt;SQN-47&lt;/code&gt; and &lt;code&gt;SQN-48&lt;/code&gt;. Their abbreviated scopes differ as follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Subject&lt;/th&gt;
&lt;th&gt;Deliverable&lt;/th&gt;
&lt;th&gt;Relationship&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQN-46&lt;/td&gt;
&lt;td&gt;Instagram publishing series&lt;/td&gt;
&lt;td&gt;Theme management and ongoing operation&lt;/td&gt;
&lt;td&gt;Parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQN-47&lt;/td&gt;
&lt;td&gt;Joining through an invitation link&lt;/td&gt;
&lt;td&gt;Five carousel images, HTML/CSS, caption, alt text&lt;/td&gt;
&lt;td&gt;Static version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQN-48&lt;/td&gt;
&lt;td&gt;The same joining guide&lt;/td&gt;
&lt;td&gt;Vertical MP4, editable source, scene images, subtitles, cover&lt;/td&gt;
&lt;td&gt;Video version&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both children cite Codex chat 2026-09-09, and the video refers to the static version. Their shared theme reflects adaptation to another format, rather than duplicate work.&lt;/p&gt;

&lt;p&gt;The Git records preserve the same relationship. The &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/e295f5cedf9f8989b36c9f308dee4dbd8cf64c3f/docs/marketing/instagram/001-invite-join/README.md" rel="noopener noreferrer"&gt;static README&lt;/a&gt; links back to &lt;code&gt;SQN-47&lt;/code&gt;, and the &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/e295f5cedf9f8989b36c9f308dee4dbd8cf64c3f/docs/marketing/instagram/001-invite-join/video/README.md" rel="noopener noreferrer"&gt;video README&lt;/a&gt; links to &lt;code&gt;SQN-48&lt;/code&gt;. The audit also read the initial production commit &lt;code&gt;560e752&lt;/code&gt;, the audio-preview commit &lt;code&gt;afcf45f&lt;/code&gt;, and the later operating documentation in &lt;code&gt;e295f5c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Combining the two would make a single Done state unable to express that the images are ready while the video's subtitles still need work. When wording recurs across conversations, compare the outputs first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve execution conditions in related parent and child tasks
&lt;/h2&gt;

&lt;p&gt;Another pair, &lt;code&gt;SQN-14&lt;/code&gt; and &lt;code&gt;SQN-27&lt;/code&gt;, both concern removing a legacy Turnstile compatibility bypass. Their distinction is an execution condition rather than a media format.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SQN-14&lt;/code&gt; describes checking adoption of the updated app before removing the compatibility code. Its child, &lt;code&gt;SQN-27&lt;/code&gt;, specifies final removal after reviewing legacy usage on or after 2026-09-10. The retrieved &lt;code&gt;parent&lt;/code&gt; relationship matched the description.&lt;/p&gt;

&lt;p&gt;Discarding the later TODO merely because both say "remove the old code" would lose the condition about allowing time for update notices to reach users. Treating them as independent implementation tasks could instead duplicate the same deletion.&lt;/p&gt;

&lt;p&gt;The parent can be read as the background and overall conditions, with the child specifying when to execute. Both were Done when retrieved, but production logs and behavior after removal were not rechecked. The state is an observed field; correctness of the code removal requires separate evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reduce a candidate to one line before importing it
&lt;/h2&gt;

&lt;p&gt;Before creating a ticket from a conversation, rewrite its candidate task in this form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service / Target of the change / Deliverable / Condition for accepting it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the joining guide, that becomes "SquadNote / invitation guidance / vertical video / current flow, subtitles, and visuals agree." Search existing tickets with that description, then read their bodies and relationships rather than only their titles.&lt;/p&gt;

&lt;p&gt;A proposed way to handle the matches is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If target, output, and acceptance conditions match, collect the additional information in the existing ticket.&lt;/li&gt;
&lt;li&gt;If the theme matches but the output differs, retain a separate related task.&lt;/li&gt;
&lt;li&gt;If it is an independently verifiable part of a larger plan, consider a parent-child relationship.&lt;/li&gt;
&lt;li&gt;If it changes a completed deliverable, retain the reason and target version before deciding whether to reopen the work or create a follow-up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Text similarity can help locate candidates. It cannot reliably establish that subtitle acceptance conditions or a rollout prerequisite match.&lt;/p&gt;

&lt;p&gt;When several conversations contribute to one candidate, append their dates and relevant decisions instead of replacing the source with the latest one. This does not require copying complete chats into the description. Leave out secrets and verification invitation URLs, and retain the information needed to follow the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Done state can hide later preparation work
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SQN-48&lt;/code&gt; was Done at retrieval time. The fixed Git &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/e295f5cedf9f8989b36c9f308dee4dbd8cf64c3f/docs/marketing/instagram/001-invite-join/video/publication-check.md" rel="noopener noreferrer"&gt;publication review&lt;/a&gt; still records work such as generating publication audio, synchronizing subtitles to speech, and verifying actual interactions.&lt;/p&gt;

&lt;p&gt;Those two observations do not prove the original ticket was closed incorrectly. Its scope may have changed between initial production and later publication preparation. They do show that Done alone is insufficient evidence that the latest asset is ready to publish.&lt;/p&gt;

&lt;p&gt;During consolidation, check for subsequent changes before treating a completed ticket as requiring no action. In this example, a handoff candidate can retain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sources: The video ticket and publication review at a fixed commit
Existing assets: A silent preview master and editable production sources
Remaining work: Publication audio, subtitle synchronization, interaction and posting checks
Completion evidence: Final-version verification and a publication record after approval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a handoff example derived from the records, not a newly created ticket. The audit did not publish anything or generate paid media.&lt;/p&gt;

&lt;p&gt;Consolidating conversation TODOs means identifying which existing deliverable should advance, and from which recorded state. In this case, the static and video outputs needed to remain distinct, while the latest Git document supplied the remaining publication preparation work.&lt;/p&gt;

</description>
      <category>plane</category>
      <category>productivity</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Verify authentication recovery before treating Configuration as the root cause</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Thu, 15 Oct 2026 02:03:05 +0000</pubDate>
      <link>https://dev.to/hirodeath/verify-authentication-recovery-before-treating-configuration-as-the-root-cause-553m</link>
      <guid>https://dev.to/hirodeath/verify-authentication-recovery-before-treating-configuration-as-the-root-cause-553m</guid>
      <description>&lt;p&gt;A return URL containing &lt;code&gt;error=Configuration&lt;/code&gt; suggests a configuration mistake. Yet the inspected Auth.js version also produces that destination when a PKCE cookie is missing. The user-facing classification does not identify the cause by itself.&lt;/p&gt;

&lt;p&gt;This investigation read SquadNote's recovery implementation and reran tests against its actual Auth.js dependency. It examines rejection, retry navigation, and diagnostic evidence before drawing a causal conclusion.&lt;/p&gt;

&lt;p&gt;The source is &lt;code&gt;circle-hub&lt;/code&gt; at &lt;code&gt;d116e8a&lt;/code&gt;, checked on September 11, 2026. No production authentication logs or user histories were collected. This is an account of recovery code and rerun verification, not proof of a production incident's cause or resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguish the internal error from Configuration
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://authjs.dev/reference/core/errors#invalidcheck" rel="noopener noreferrer"&gt;Auth.js's InvalidCheck reference&lt;/a&gt; covers PKCE, state, or nonce checks that cannot be performed. Unavailable cookies can produce it, as can configuration issues.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/d116e8a343e8d9e7ddd3d1985efe590fa1901868/apps/web/src/server/auth/auth-core-recovery.test.ts" rel="noopener noreferrer"&gt;repository regression test&lt;/a&gt; resolves &lt;code&gt;@auth/core&lt;/code&gt; through the application's &lt;code&gt;next-auth&lt;/code&gt; dependency. It does not substitute a separately installed latest release.&lt;/p&gt;

&lt;p&gt;A synthetic callback without its PKCE cookie produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP status&lt;/td&gt;
&lt;td&gt;302&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirect destination&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/sign-in/error?error=Configuration&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal type passed to the logger&lt;/td&gt;
&lt;td&gt;&lt;code&gt;InvalidCheck&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fixture deliberately omitted the cookie, so its cause is known. Seeing the same internal type in production would still require investigating why the cookie did not arrive. Expiration, another initiation, and browser behavior cannot be distinguished from that display alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  First restore a path from failure to a fresh start
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/d116e8a343e8d9e7ddd3d1985efe590fa1901868/docs/auth-recovery-diagnostics.md" rel="noopener noreferrer"&gt;recovery design record&lt;/a&gt; and history show the recovery screen in &lt;code&gt;061a5f2&lt;/code&gt;, repeated-submission protection in &lt;code&gt;2b88c31&lt;/code&gt;, and diagnostics in &lt;code&gt;7f180ad&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The recovery screen presents sign-in and public help without depending on &lt;code&gt;auth()&lt;/code&gt; or the database. Displaying an authentication failure does not require successfully repeating the same authentication dependency.&lt;/p&gt;

&lt;p&gt;Return destinations are restricted to safe paths on the same origin. Tests preserve invitation-page returns while rejecting external redirection and authentication-page loops. The original callback URL is not reused unconditionally.&lt;/p&gt;

&lt;p&gt;Two new initiation requests sent to the actual Auth.js implementation produced different PKCE challenges, &lt;code&gt;S256&lt;/code&gt;, and fresh PKCE cookies. However, the initiation fixture uses the internal CSRF-skip option. It does not verify browser CSRF protection or a round trip through the OAuth provider, and no production setting was changed to disable CSRF checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reject a second start before React rerenders
&lt;/h2&gt;

&lt;p&gt;Disabling a button visually can leave a window before React rerenders in which another button can be pressed. The implementation uses a synchronous guard shared by Google and Apple initiation on the same screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createSubmissionGuard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;submitting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;submitting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;submitting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;submitting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests against the &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/d116e8a343e8d9e7ddd3d1985efe590fa1901868/apps/web/src/app/sign-in/submission-guard.ts" rel="noopener noreferrer"&gt;actual guard&lt;/a&gt; reject immediate reacquisition, allow acquisition after the reset used for failure or history restoration, and keep separate instances independent.&lt;/p&gt;

&lt;p&gt;Its scope is repeated submission from one screen. It does not serialize initiation across tabs or browsers, nor remove every possible cause of missing cookies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correlate how far the flow progressed
&lt;/h2&gt;

&lt;p&gt;The diagnostics investigate initiation and callback boundaries without storing authentication values. The principal events mean:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What it establishes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auth.sign_in_started&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A start request occurred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auth.authorization_redirect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initiation generated a destination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auth.callback_received&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A callback arrived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auth.sign_in_succeeded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auth.js emitted its signIn event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auth.server_error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Failure and diagnostic information were recorded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Initiation and destination generation do not mean authentication succeeded. In particular, the server action's &lt;code&gt;cookiesAfterStart&lt;/code&gt; observes the cookie jar at that time. Existing cookies can be present, so presence alone does not establish fresh issuance.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;requestId&lt;/code&gt; identifies a failing request; &lt;code&gt;attemptId&lt;/code&gt; supports correlation with initiation. The latter marker is client-originated diagnostic information and does not authorize authentication. It represents the browser's most recent initiation, so it does not prove the same OAuth transaction across multiple tabs.&lt;/p&gt;

&lt;p&gt;Absent, expired, or provider-mismatched markers can prevent correlation. Record why correlation failed instead of rephrasing that as “no initiation happened.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Add diagnostics without weakening authentication checks
&lt;/h2&gt;

&lt;p&gt;Diagnostic tests verify that synthetic sensitive values, including cookie values, authorization codes, tokens, raw User-Agent strings, and IPs, do not appear in logs. Cookie observations retain only presence for specific names, and paths are restricted to authentication routes.&lt;/p&gt;

&lt;p&gt;Tests also check isolation between concurrent requests, preservation of authentication responses when diagnostics or notifications fail, and preservation of the original Auth.js response cookies. The change does not obtain successful logins by relaxing PKCE, state, or nonce requirements.&lt;/p&gt;

&lt;p&gt;Alert suppression operates within a Worker instance; it is not a delivery guarantee across all distributed instances. The diagnostic cookie is not a session replacement either. Neither mechanism belongs in the decision that authentication has succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 47 tests and the remaining environment checks
&lt;/h2&gt;

&lt;p&gt;The run passed 47 tests across 8 files copied from the fixed commit. It used Node.js &lt;code&gt;v24.15.0&lt;/code&gt;, Vitest &lt;code&gt;4.1.4&lt;/code&gt;, Next.js &lt;code&gt;15.5.15&lt;/code&gt;, next-auth &lt;code&gt;5.0.0-beta.25&lt;/code&gt;, and its Auth.js core dependency &lt;code&gt;0.37.2&lt;/code&gt;. The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/batch05-circle-tests.py" rel="noopener noreferrer"&gt;runner&lt;/a&gt; and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-05/auth-tests.json" rel="noopener noreferrer"&gt;results&lt;/a&gt; are preserved.&lt;/p&gt;

&lt;p&gt;Some tests invoke the actual Auth.js implementation; others mock route or notification dependencies. Real Google or Apple accounts, Safari or embedded browsers, multiple tabs, and manual browser-cookie deletion were not tested in this run. Historical UI verification documented in the repository is also separate from these rerun results.&lt;/p&gt;

&lt;p&gt;Verify the failure classification, fresh initiation, safe return destination, same-screen repeat protection, and the limits of log correlation in order. While the cause remains uncertain, record the conditions for retry and the questions requiring further investigation.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>nextjs</category>
      <category>testing</category>
      <category>debugging</category>
    </item>
    <item>
      <title>From 63 seconds to 60: checking a game trailer against its timeline and video files</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Wed, 14 Oct 2026 02:22:13 +0000</pubDate>
      <link>https://dev.to/hirodeath/from-63-seconds-to-60-checking-a-game-trailer-against-its-timeline-and-video-files-1b6h</link>
      <guid>https://dev.to/hirodeath/from-63-seconds-to-60-checking-a-game-trailer-against-its-timeline-and-video-files-1b6h</guid>
      <description>&lt;p&gt;Even a short game trailer fills up quickly with a title, combat, upgrades, bosses, and a destination to play. Removing a few seconds at the end does not change what viewers encounter first.&lt;/p&gt;

&lt;p&gt;VOLT NOMAD's history retains an initial trailer configured for 63 seconds and a second version reorganized around 60 seconds. The change also moved combat to the opening. This article compares the code-defined sequence with the actual video files.&lt;/p&gt;

&lt;p&gt;The investigation inspected fixed revisions and Git-tracked MP4 files on September 11, 2026. It did not regenerate the trailer or measure audience retention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combat began at 13 seconds in V1 and at the opening in V2
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/game-jam-lab/blob/bf5c7e7/events/2026-ai-browser-game-jam-4/godot/tools/trailer_capture.gd" rel="noopener noreferrer"&gt;initial capture code&lt;/a&gt; at &lt;code&gt;bf5c7e7&lt;/code&gt; begins with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configured interval&lt;/th&gt;
&lt;th&gt;Initial content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.0 to 3.5 seconds&lt;/td&gt;
&lt;td&gt;Key art&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.5 to 8.0 seconds&lt;/td&gt;
&lt;td&gt;Title screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.0 to 13.0 seconds&lt;/td&gt;
&lt;td&gt;Route selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13.0 to 22.0 seconds&lt;/td&gt;
&lt;td&gt;Combat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/game-jam-lab/blob/f074703848586828b6a5acc0e465ccdd2c0d5244/events/2026-ai-browser-game-jam-4/godot/tools/trailer_capture.gd" rel="noopener noreferrer"&gt;capture code containing V2&lt;/a&gt; at &lt;code&gt;f074703&lt;/code&gt; opens with early combat followed by upgraded combat:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configured interval&lt;/th&gt;
&lt;th&gt;V2 content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.0 to 4.5 seconds&lt;/td&gt;
&lt;td&gt;Initial combat: click, damage, CHARGE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.5 to 10.0 seconds&lt;/td&gt;
&lt;td&gt;Upgraded combat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.0 to 16.0 seconds&lt;/td&gt;
&lt;td&gt;Gear tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16.0 to 21.0 seconds&lt;/td&gt;
&lt;td&gt;Route selection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ordering in the code verifies a change toward showing what an action produces at the start. It does not establish an improvement in viewer understanding or view counts.&lt;/p&gt;

&lt;p&gt;The useful editing decision is to reconsider which action belongs first before shortening every asset a little.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the interval table to drive capture
&lt;/h2&gt;

&lt;p&gt;V2's &lt;code&gt;beats&lt;/code&gt; array holds start and end times, a scene ID, captions, and other presentation data. Runtime code chooses the interval from elapsed time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;beat_index_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;beats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changing intervals configures the game state and presentation for capture. Reaching &lt;code&gt;DURATION&lt;/code&gt; ends the process.&lt;/p&gt;

&lt;p&gt;This function selects intervals by their end times. It does not separately use start times to create empty gaps. If one start differs from the preceding end, the table's description and the execution can disagree.&lt;/p&gt;

&lt;p&gt;The verification checked all 11 intervals: the first starts at 0, each meets the preceding interval without a gap or overlap, and the last ends at 60. The unchanged function was also extracted into Godot to verify selection just before and exactly at each boundary. Including the start and end, 22 assertions passed. These checks cover both interval consistency and switching positions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using game rendering does not mean recording ordinary play
&lt;/h2&gt;

&lt;p&gt;The capture node instantiates the real &lt;code&gt;ChargeClicker&lt;/code&gt;. The script also changes progression state and invokes attacks at predetermined intervals.&lt;/p&gt;

&lt;p&gt;Persistence is disabled, the language is English, and capture audio is muted. The footage uses the game's rendering, but it is not a person playing an ordinary run from beginning to end.&lt;/p&gt;

&lt;p&gt;In V2, the game node is hidden for &lt;code&gt;prime_art&lt;/code&gt; and &lt;code&gt;final&lt;/code&gt;. Summing every other interval produced &lt;code&gt;51.5&lt;/code&gt; seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interval category&lt;/th&gt;
&lt;th&gt;Sum in the code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Game node visible&lt;/td&gt;
&lt;td&gt;51.5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cinematic image&lt;/td&gt;
&lt;td&gt;3.5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final title and destination&lt;/td&gt;
&lt;td&gt;5.0 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;60.0 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a sum of scripted intervals. It is not a frame-by-frame review of the finished video measuring when the game is visible. If using the number in a production write-up, preserve how it was calculated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the final file after combining music and video
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/game-jam-lab/blob/f074703848586828b6a5acc0e465ccdd2c0d5244/events/2026-ai-browser-game-jam-4/submission/trailer/README.md" rel="noopener noreferrer"&gt;production README&lt;/a&gt; describes combining Godot's output with the selected music. It records choosing candidate A because its quiet/loud transitions aligned with the scene changes. This investigation did not listen to those candidates again.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/game-jam-lab/blob/f074703848586828b6a5acc0e465ccdd2c0d5244/events/2026-ai-browser-game-jam-4/tools/compose-trailer.sh" rel="noopener noreferrer"&gt;composition script&lt;/a&gt; scales video to 1920×1080 and outputs a 30 fps, 60-second MP4. Audio receives &lt;code&gt;volume=0.63&lt;/code&gt; and a &lt;code&gt;0.8&lt;/code&gt;-second fade beginning at &lt;code&gt;59.2&lt;/code&gt; seconds.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ffmpeg.org/ffmpeg-filters.html#volume" rel="noopener noreferrer"&gt;FFmpeg volume specification&lt;/a&gt; defines that value as an input multiplier. &lt;code&gt;0.63&lt;/code&gt; corresponds to approximately -4 dB. It does not automatically normalize to a target loudness, so it is not a measured LUFS result for the final video.&lt;/p&gt;

&lt;p&gt;The script also refuses to overwrite an existing output path. That supports comparing V2 while retaining V1. The composition script itself was not rerun during this investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A configured 63 seconds and a file lasting 63.033333 seconds
&lt;/h2&gt;

&lt;p&gt;The MP4 files were extracted from Git into temporary files and inspected with ffprobe &lt;code&gt;9.0.1&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;th&gt;Frame rate&lt;/th&gt;
&lt;th&gt;format.duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;1280×720&lt;/td&gt;
&lt;td&gt;30/1&lt;/td&gt;
&lt;td&gt;63.033333 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V2&lt;/td&gt;
&lt;td&gt;1920×1080&lt;/td&gt;
&lt;td&gt;30/1&lt;/td&gt;
&lt;td&gt;60.000000 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both contain H.264 video and stereo AAC audio at 48 kHz. V1's configured 63 seconds and its file duration are not identical. This metadata inspection alone does not locate the cause of that difference.&lt;/p&gt;

&lt;p&gt;The README's timing table also rounds some times to whole seconds. When adjusting a caption or cut by frame, inspect the code's &lt;code&gt;4.5&lt;/code&gt; seconds and the footage instead of relying on the descriptive “00:04.”&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/game-input-media-evidence.py" rel="noopener noreferrer"&gt;reproduction script&lt;/a&gt; and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-05/game-media.json" rel="noopener noreferrer"&gt;recorded results&lt;/a&gt; preserve the intervals, file hashes, and ffprobe output. Neither source videos nor publication destinations were changed.&lt;/p&gt;

&lt;p&gt;Use the interval table to decide the order of ideas and the finished file to verify the resulting order and duration. Keep the configured times alongside the durations verified in the output.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>ffmpeg</category>
      <category>video</category>
    </item>
    <item>
      <title>Audit a game soundtrack by selection records, playback slots, and actual files</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Tue, 13 Oct 2026 04:08:13 +0000</pubDate>
      <link>https://dev.to/hirodeath/audit-a-game-soundtrack-by-selection-records-playback-slots-and-actual-files-4cai</link>
      <guid>https://dev.to/hirodeath/audit-a-game-soundtrack-by-selection-records-playback-slots-and-actual-files-4cai</guid>
      <description>&lt;p&gt;Soundtrack candidates, selected compositions, stored files, and playback assignments gradually diverge. Replacing map music can leave the old file in storage, while two scenes may share one recording. “There are 18 tracks” does not explain what was counted.&lt;/p&gt;

&lt;p&gt;Comparing VOLT NOMAD's production notes with its code found 18 BGM slots referring to 17 distinct audio files. Including the victory jingle makes 18 referenced files. The audio directory contains 19.&lt;/p&gt;

&lt;p&gt;This article follows selection records through to playback assignments. The source is &lt;code&gt;game-jam-lab&lt;/code&gt; at &lt;code&gt;f074703&lt;/code&gt;, inspected on September 11, 2026. No listening comparison or music regeneration was performed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate selection reasons from implemented assignments
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/game-jam-lab/blob/f074703848586828b6a5acc0e465ccdd2c0d5244/events/2026-ai-browser-game-jam-4/docs/SUNO_MUSIC_BRIEF.md" rel="noopener noreferrer"&gt;music production brief&lt;/a&gt; records purpose, title, Suno song ID, the selected A/B candidate, and the game file.&lt;/p&gt;

&lt;p&gt;The enemy-specific expansion notes say that some initial candidates were too short and were removed from consideration. Tracks were regenerated with a specified duration, followed by another attempt for candidates that still ended early. The records identify an initial Relay Hydra A at 1:54 and Grid Leech A at 2:34.&lt;/p&gt;

&lt;p&gt;Those durations are historical production records. This investigation did not retrieve and measure every Suno candidate again. They support the narrower observation that duration was a selection criterion at that stage.&lt;/p&gt;

&lt;p&gt;The history also progresses from candidate records in &lt;code&gt;5ab837b&lt;/code&gt; to enemy-specific music in &lt;code&gt;290d302&lt;/code&gt;. That helps distinguish stored candidates from music that reached the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the name to the playback key
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/game-jam-lab/blob/f074703848586828b6a5acc0e465ccdd2c0d5244/events/2026-ai-browser-game-jam-4/godot/games/charge_clicker/charge_clicker.gd" rel="noopener noreferrer"&gt;game's music code&lt;/a&gt; contains two dictionaries with different roles.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EncounterBGMKeys&lt;/code&gt; maps encounter or form IDs to BGM keys. &lt;code&gt;BGMStreams&lt;/code&gt; maps those keys to audio files. A few inspected relationships are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Encounter ID&lt;/th&gt;
&lt;th&gt;BGM key&lt;/th&gt;
&lt;th&gt;Referenced file&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gearmaw&lt;/td&gt;
&lt;td&gt;hunt&lt;/td&gt;
&lt;td&gt;piston_hunt_loop.ogg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vaultback&lt;/td&gt;
&lt;td&gt;vaultback&lt;/td&gt;
&lt;td&gt;blue_vault_pulse.ogg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;phase_mantis&lt;/td&gt;
&lt;td&gt;phase_mantis&lt;/td&gt;
&lt;td&gt;critical_parallax.ogg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prime_current_form_1&lt;/td&gt;
&lt;td&gt;prime_current_form_1&lt;/td&gt;
&lt;td&gt;prime_current_crownless_protocol.mp3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Following both stages avoids mistaking a differently named key for an unused composition. Gearmaw's dedicated track uses the generic key &lt;code&gt;hunt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;desired_bgm_key()&lt;/code&gt; makes the actual selection. The open title or gallery, ordinary or boss combat, and the final boss form affect its result. A dictionary entry alone does not establish that the intended scene can reach it. The selection branches were inspected too, but every scene was not played through during this investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  18 slots, 17 BGM files, 18 files with the jingle
&lt;/h2&gt;

&lt;p&gt;The automated count of dictionaries and Git-tracked audio produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Counted object&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keys in &lt;code&gt;BGMStreams&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distinct audio files referenced by those keys&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referenced files including the separate victory jingle&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stored files in the target audio directory&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both &lt;code&gt;singularity&lt;/code&gt; and &lt;code&gt;ending_world&lt;/code&gt; use &lt;code&gt;arch_singularity.ogg&lt;/code&gt;. Separate scene slots do not create two compositions.&lt;/p&gt;

&lt;p&gt;The previous map track, &lt;code&gt;subterranean_hunt.ogg&lt;/code&gt;, remains stored but is absent from the inspected BGM dictionary's references. The current map key points to &lt;code&gt;six_core_descent.mp3&lt;/code&gt;. This reflects retaining an earlier track after replacement; the investigation did not delete the unreferenced file.&lt;/p&gt;

&lt;p&gt;The production brief's “18 tracks” corresponds to distinct BGM files plus the victory jingle. Keeping separate counts for keys, files, and stored material helps detect duplicates and omissions during cleanup or a port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare measured file duration with rounded production notes
&lt;/h2&gt;

&lt;p&gt;ffprobe inspected 19 audio files extracted from Git. Every audio stream was 48 kHz with 2 channels. Selected file durations were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;ffprobe duration in seconds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;blue_vault_pulse.ogg&lt;/td&gt;
&lt;td&gt;180.040000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cascade_trinity.ogg&lt;/td&gt;
&lt;td&gt;179.880000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;critical_parallax.ogg&lt;/td&gt;
&lt;td&gt;179.800000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nomad_victory_signal.mp3&lt;/td&gt;
&lt;td&gt;12.973500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“3:00” and “0:13” in the production table are rounded descriptions. They are not precise loop points or stop times. The values above are also file durations reported by ffprobe, not listening measurements of the first and last audible sounds.&lt;/p&gt;

&lt;p&gt;The brief records mastering the initial tracks to approximately -18 LUFS. However, the original WAV files are not in this repository. This investigation neither reproduced that mastering pass nor established that every current track has equal loudness. Later MP3 additions were not assumed to have received the same processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record loop configuration separately from listening approval
&lt;/h2&gt;

&lt;p&gt;The implementation enables looping for Ogg and MP3 when switching BGM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;next_stream&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AudioStreamOggVorbis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_stream&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;AudioStreamOggVorbis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;next_stream&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AudioStreamMP3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_stream&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;AudioStreamMP3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://docs.godotengine.org/en/stable/classes/class_audiostreammp3.html" rel="noopener noreferrer"&gt;Godot's AudioStreamMP3 reference&lt;/a&gt; documents its &lt;code&gt;loop&lt;/code&gt; property. The code requests repetition, but musical continuity between the end and the beginning is a separate question. This investigation did not listen for seam noise, silence, or beat misalignment.&lt;/p&gt;

&lt;p&gt;Two AudioStreamPlayer instances handle scene changes with a configured crossfade of &lt;code&gt;0.85&lt;/code&gt; seconds. The short victory jingle has a separate player and does not enter the full-track BGM loop. These observations describe playback code, not measured listening comfort or mix quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep enough evidence for the next replacement
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/game-input-media-evidence.py" rel="noopener noreferrer"&gt;verification script&lt;/a&gt; reads dictionaries and audio from the fixed commit and records reference existence, counts, SHA-256 hashes, and ffprobe data. The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-05/game-media.json" rel="noopener noreferrer"&gt;saved results&lt;/a&gt; used ffprobe &lt;code&gt;9.0.1&lt;/code&gt;. No audio conversion or upload took place.&lt;/p&gt;

&lt;p&gt;For the next replacement, the same record can hold the candidate ID, selection reason, game key, file, and the extent of listening verification. The record then identifies the playback destination and any checks still pending.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>audio</category>
      <category>ai</category>
    </item>
    <item>
      <title>Define one input action per screen across mouse, touch, and gamepad controls</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Mon, 12 Oct 2026 02:52:55 +0000</pubDate>
      <link>https://dev.to/hirodeath/define-one-input-action-per-screen-across-mouse-touch-and-gamepad-controls-2g4b</link>
      <guid>https://dev.to/hirodeath/define-one-input-action-per-screen-across-mouse-touch-and-gamepad-controls-2g4b</guid>
      <description>&lt;p&gt;Pushing a stick right should move the selection once. Treat every event received while the stick remains tilted as “next,” and the selection keeps jumping. Charging an attack, meanwhile, needs both the press and the release.&lt;/p&gt;

&lt;p&gt;Supporting several input devices requires defining what counts as one action on each screen. This investigation read a fixed version of VOLT NOMAD and tested its gamepad direction conversion and button remapping in Godot.&lt;/p&gt;

&lt;p&gt;The source is &lt;code&gt;game-jam-lab&lt;/code&gt; at &lt;code&gt;f074703&lt;/code&gt;. Verification took place locally on September 11, 2026; it was not a playtest across every supported device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not invent an implementation sequence from the finished feature list
&lt;/h2&gt;

&lt;p&gt;The history records shared gamepad settings in &lt;code&gt;ef5cb81&lt;/code&gt; on August 1, 2026, and the Project Charge prototype in &lt;code&gt;b3be93e&lt;/code&gt; on August 2. Combat, gear trees, dialogue, and gallery screens subsequently grew around that code.&lt;/p&gt;

&lt;p&gt;This does not support a linear story of finishing mouse support, then touch, then gamepad support. The implementation inherited shared input settings and added handlers as screens appeared.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/game-jam-lab/blob/f074703848586828b6a5acc0e465ccdd2c0d5244/events/2026-ai-browser-game-jam-4/godot/games/charge_clicker/charge_clicker.gd" rel="noopener noreferrer"&gt;input code at the inspected revision&lt;/a&gt; first selects the open screen, dispatches to its handler, and returns. An action closing dialogue should not continue into the combat handling later in the same function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Select the screen before interpreting the input
&lt;/h2&gt;

&lt;p&gt;At its entry, &lt;code&gt;_unhandled_input()&lt;/code&gt; calls &lt;code&gt;set_input_as_handled()&lt;/code&gt;. It then routes events to dialogue, settings, the title, the gear tree, and other screens.&lt;/p&gt;

&lt;p&gt;Marking an event handled in the Viewport and stopping execution inside the current function are separate jobs. The former does not automatically return from that function; the branch's &lt;code&gt;return&lt;/code&gt; matters too. &lt;a href="https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html" rel="noopener noreferrer"&gt;Godot's InputEvent documentation&lt;/a&gt; explains event delivery and the handled flag.&lt;/p&gt;

&lt;p&gt;Events reaching ordinary combat follow these paths:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Handling in the implementation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Left mouse button&lt;/td&gt;
&lt;td&gt;Check position on press; end charging on release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Touch&lt;/td&gt;
&lt;td&gt;Pass the press position to the same operations; end charging on release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyboard&lt;/td&gt;
&lt;td&gt;Send events without echo to a dedicated handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gamepad button&lt;/td&gt;
&lt;td&gt;Send events to a dedicated handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Left stick&lt;/td&gt;
&lt;td&gt;Convert motion into a single direction action before moving selection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These paths were inspected in source. This run did not test browser-generated mouse events from touch or multiple simultaneous fingers. Having both mouse and touch branches does not establish an absence of duplicate input on every device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait for neutral before emitting another direction
&lt;/h2&gt;

&lt;p&gt;This is the direction-selection function. It was extracted unchanged from the game and executed in a small class containing only the required &lt;code&gt;controller_axis_latch&lt;/code&gt; state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;controller_motion_direction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InputEventJoypadMotion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Vector2i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector2i&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ZERO&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;JOY_AXIS_LEFT_X&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;absf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axis_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;controller_axis_latch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;controller_axis_latch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;controller_axis_latch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axis_value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;controller_axis_latch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;JOY_AXIS_LEFT_Y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;absf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axis_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;controller_axis_latch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;controller_axis_latch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;controller_axis_latch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axis_value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;controller_axis_latch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning below an absolute axis value of &lt;code&gt;0.45&lt;/code&gt; clears the latch. Otherwise, the function emits a direction only when the stick first leaves the unlatched state.&lt;/p&gt;

&lt;p&gt;Synthetic horizontal-axis events produced this sequence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis value&lt;/th&gt;
&lt;th&gt;Returned direction&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Within the neutral region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;td&gt;Right&lt;/td&gt;
&lt;td&gt;First action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Still tilted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-0.8&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No neutral event has arrived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Latch cleared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-0.8&lt;/td&gt;
&lt;td&gt;Left&lt;/td&gt;
&lt;td&gt;Next action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Even jumping directly from a positive to a negative value does not emit the opposite direction without a neutral event. This function has no timed repeat while held. Changing that behavior to suit physical controls requires a different rule for repeated input.&lt;/p&gt;

&lt;p&gt;The full check covered 10 events, including the vertical axis independently emitting an action while the horizontal axis remained latched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remapping must preserve other input types
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/game-jam-lab/blob/f074703848586828b6a5acc0e465ccdd2c0d5244/events/2026-ai-browser-game-jam-4/godot/shared/controller_bindings.gd" rel="noopener noreferrer"&gt;shared controller settings class&lt;/a&gt; swaps assignments when a new button is already assigned to another action. That avoids binding two actions to the same button.&lt;/p&gt;

&lt;p&gt;When updating InputMap, it removes only &lt;code&gt;InputEventJoypadButton&lt;/code&gt; events. It does not clear keyboard events assigned to the same action.&lt;/p&gt;

&lt;p&gt;Tests against the actual class verified that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Moving primary to secondary's existing button moves secondary to primary's previous button.&lt;/li&gt;
&lt;li&gt;Unsupported buttons and unknown action IDs are rejected without changing the bindings.&lt;/li&gt;
&lt;li&gt;Changing attack's gamepad button preserves a previously assigned Space key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks cover the shared class. They do not prove that every key assignment automatically reaches every VOLT NOMAD screen. The game also performs its own button checks, so each screen's route needs inspection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the scope of support precisely
&lt;/h2&gt;

&lt;p&gt;The runtime was Godot &lt;code&gt;4.6.2.stable.official.71f334935&lt;/code&gt; in headless mode. The test did not read the game's saved progress, and writing controller settings was disabled. The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/game-input-media-evidence.py" rel="noopener noreferrer"&gt;reproduction script&lt;/a&gt; and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-05/game-input.json" rel="noopener noreferrer"&gt;input results&lt;/a&gt; are recorded.&lt;/p&gt;

&lt;p&gt;This run verified conversion and remapping after synthetic events arrived. Browser focus, event generation from touch, controller connection and disconnection, button labels, and comfort during extended play require separate checks.&lt;/p&gt;

&lt;p&gt;An input support checklist should record the screen, the meaning of press, release, and continued input, and the sequence actually tested. Record code-level checks separately from actions a person has verified on a device.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>testing</category>
      <category>input</category>
    </item>
    <item>
      <title>An outbox preserves notification intent; delivery deduplication is a separate boundary</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Wed, 07 Oct 2026 01:39:03 +0000</pubDate>
      <link>https://dev.to/hirodeath/an-outbox-preserves-notification-intent-delivery-deduplication-is-a-separate-boundary-510h</link>
      <guid>https://dev.to/hirodeath/an-outbox-preserves-notification-intent-delivery-deduplication-is-a-separate-boundary-510h</guid>
      <description>&lt;p&gt;Saving a record before calling a notification API leaves a gap where execution can stop. The data exists, but there is no record that a notification was required. Reversing the order can notify someone about data that never saved successfully.&lt;/p&gt;

&lt;p&gt;The outbox pattern saves business data and an event to deliver later in the same database transaction. That does not, by itself, make external delivery happen once.&lt;/p&gt;

&lt;p&gt;This experiment put note creation and outbox insertion into one local D1 batch, then tested a consumer stopping after successful transmission. It ran on September 11, 2026, with a fake external provider. No actual notifications or production changes were made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persist notification intent first
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html" rel="noopener noreferrer"&gt;AWS's transactional outbox guidance&lt;/a&gt; addresses inconsistencies between database updates and message delivery. Business data and the event are stored in one transaction and delivered afterward. Duplicate delivery still needs handling.&lt;/p&gt;

&lt;p&gt;The experiment placed a &lt;code&gt;note&lt;/code&gt; table and this &lt;code&gt;outbox&lt;/code&gt; table in the same D1 database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;note_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'note.created'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sent&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;event_id&lt;/code&gt; identifies the persisted event rather than being regenerated for every delivery attempt. The JSON &lt;code&gt;payload&lt;/code&gt; contains the note ID and its title at that point. Retries do not reread the current note, so the same event retains its contents.&lt;/p&gt;

&lt;p&gt;One batch receives the two inserts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO note VALUES (?, ?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;actor-a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO outbox(event_id,note_id,kind,payload) VALUES (?, ?, ?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an excerpt from the &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/d1-idempotency-outbox.mjs" rel="noopener noreferrer"&gt;verification code&lt;/a&gt;. The transaction provided by D1's &lt;a href="https://developers.cloudflare.com/d1/worker-api/d1-database/#batch" rel="noopener noreferrer"&gt;batch API&lt;/a&gt; covers those database writes. Sending to an external service does not participate in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does a failed event insert leave the note behind?
&lt;/h2&gt;

&lt;p&gt;The outbox CHECK constraint permits only &lt;code&gt;note.created&lt;/code&gt;. Passing an invalid kind forced the second INSERT to fail.&lt;/p&gt;

&lt;p&gt;The result was 0 notes and 0 outbox events. The earlier note insertion was rolled back too.&lt;/p&gt;

&lt;p&gt;A valid creation produced 1 note and 1 outbox event before the consumer ran. Calling the consumer afterward delivered 1 event to the fake provider. This verified that delivery can start separately from business creation using the event stored in the database.&lt;/p&gt;

&lt;p&gt;That differs from merely deferring a notification Promise: the input required to resume delivery exists as a database record. However, this local experiment disabled persistence. It did not restart an actual process or host to test storage durability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Successful transmission can still precede a missing sent record
&lt;/h2&gt;

&lt;p&gt;The experimental consumer follows this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read one unsent event
Pass event_id and payload to the provider
Wait for successful transmission
Update outbox.sent to 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the consumer stops before that final update, the provider has processed the event while the outbox still marks it unsent.&lt;/p&gt;

&lt;p&gt;The test threw immediately after the fake provider returned success. Calling the consumer again against the same database transmitted that event again. It simulated interruption between transmission and recording with an exception rather than forcibly terminating a process.&lt;/p&gt;

&lt;p&gt;Without provider deduplication, there were 2 calls and 2 operations counted as deliveries. The outbox's sent flag alone did not prevent the duplicate in this interval.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;sent&lt;/code&gt; to 1 before transmission would create the opposite problem: a failed send could disappear from the retry set. Reordering one flag cannot atomically align the provider's result with the database's result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A receiver that recognizes the event can consolidate retries
&lt;/h2&gt;

&lt;p&gt;The next case used a fake provider that treated the same &lt;code&gt;event_id&lt;/code&gt; and payload as an existing result. The test would fail if an identical ID arrived with different contents.&lt;/p&gt;

&lt;p&gt;After the same interruption and retry, the provider received 2 calls but counted only 1 delivery:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Database or provider result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Constraint failure on outbox INSERT&lt;/td&gt;
&lt;td&gt;0 notes, 0 events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commit succeeds before consumer starts&lt;/td&gt;
&lt;td&gt;1 note, 1 event, 1 delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interruption after send, no provider deduplication&lt;/td&gt;
&lt;td&gt;2 calls, 2 deliveries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same interruption, provider deduplicates by event_id&lt;/td&gt;
&lt;td&gt;2 calls, 1 delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After retry, &lt;code&gt;sent&lt;/code&gt; was 1. A further consumer call also confirmed that no unsent event remained.&lt;/p&gt;

&lt;p&gt;The provider is a fake running inside Node.js. Its deduplication records never expire, and it does not represent the guarantee of an actual notification service. A real integration must verify which key the receiver remembers, for how long, and within which scope.&lt;/p&gt;

&lt;p&gt;If the receiver cannot make the effect happen once, design the consuming experience for possible duplicates. Introducing an outbox is insufficient evidence for exactly-once delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the single-consumer experiment from operational requirements
&lt;/h2&gt;

&lt;p&gt;The environment was Node.js &lt;code&gt;v24.15.0&lt;/code&gt;, Wrangler &lt;code&gt;4.81.1&lt;/code&gt;, and Miniflare &lt;code&gt;4.20260409.0&lt;/code&gt;. Remote D1 bindings were disabled, with synthetic data and the fake provider used throughout.&lt;/p&gt;

&lt;p&gt;The implementation calls one consumer sequentially. It has no claim or lease preventing multiple workers from selecting the same unsent row, no retry schedule, no failure limit, and no dead-letter queue. These tests do not justify scaling it directly to multiple consumers.&lt;/p&gt;

&lt;p&gt;In operation, expose the pending-event count, longest waiting time, and failure reasons. If the provider stops, preserve events for retries and investigation. When changing the payload schema, also check whether the new consumer can still read old pending events.&lt;/p&gt;

&lt;p&gt;This experiment verified atomic storage of business data and delivery intent, retrieval before delivery, and redelivery after interruption between sending and recording. An outbox closes the gap in which business data changes while notification intent is lost. Preventing duplicates beyond that boundary also requires a design for the receiver of the same event.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>database</category>
      <category>distributed</category>
      <category>testing</category>
    </item>
    <item>
      <title>An idempotency key needs stored results: testing retries and concurrent requests</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Tue, 06 Oct 2026 03:28:58 +0000</pubDate>
      <link>https://dev.to/hirodeath/an-idempotency-key-needs-stored-results-testing-retries-and-concurrent-requests-24bh</link>
      <guid>https://dev.to/hirodeath/an-idempotency-key-needs-stored-results-testing-retries-and-concurrent-requests-24bh</guid>
      <description>&lt;p&gt;When a create request produces no response, the client cannot distinguish failure before creation from a committed creation whose response was lost. Retrying can create a second record.&lt;/p&gt;

&lt;p&gt;An idempotency key connects that retry to the same logical request. Preventing duplication also requires deciding how the server stores the key, input, and result.&lt;/p&gt;

&lt;p&gt;This experiment put note creation and result storage into one local D1 &lt;code&gt;batch()&lt;/code&gt;, then checked lost responses, concurrency, mismatched inputs, and deleted records. The investigation ran on September 11, 2026. It is a demonstration implementation, not a rollout to an existing application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define what the same key identifies
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe's idempotent-request documentation&lt;/a&gt; describes saving the first request's status code and body and returning that result for the same key. It also compares inputs and treats reuse after the record is removed as a new request.&lt;/p&gt;

&lt;p&gt;That behavior requires more than a header name. It is also Stripe's contract: adding the same header name to a custom API does not automatically implement storage or comparison.&lt;/p&gt;

&lt;p&gt;The experiment defined key scope using three components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authenticated actor / operation and version / client key
actor-a / create-note:v1 / key-a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another actor can use &lt;code&gt;key-a&lt;/code&gt; for a separate creation. The same actor retrying the same operation returns to the original input and result. The actor is a trusted argument to the experimental function; HTTP authentication was not implemented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store an input fingerprint and the response result
&lt;/h2&gt;

&lt;p&gt;The result table was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;request_result&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;operation&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_key&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;fingerprint&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fingerprint&lt;/code&gt; is the SHA-256 of JSON containing the single experimental input, &lt;code&gt;title&lt;/code&gt;. This is not a general normalization algorithm for arbitrary JSON. A real API must include every input that affects the operation.&lt;/p&gt;

&lt;p&gt;If the key already exists, the function compares fingerprints and returns the stored result when they match. A mismatch produces an error carrying status &lt;code&gt;409&lt;/code&gt; in this implementation. That is a demonstration-specific choice, not an attempt to reproduce Stripe's error contract.&lt;/p&gt;

&lt;p&gt;For a new key, the function generates a note ID and stores both the note and the response containing that ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO request_result VALUES (?, ?, ?, ?, ?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO note VALUES (?, ?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;body&lt;/code&gt; contains the generated ID and title. The surrounding &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/d1-idempotency-outbox.mjs" rel="noopener noreferrer"&gt;experiment code&lt;/a&gt; also implements lookup, replay, and rereading after a write conflict.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developers.cloudflare.com/d1/worker-api/d1-database/#batch" rel="noopener noreferrer"&gt;D1 batch contract&lt;/a&gt; provides the transaction boundary for the two writes. This avoids preserving a successful response when creation of its note failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  An existence check alone does not handle concurrency
&lt;/h2&gt;

&lt;p&gt;Two requests can both read that no key exists. The test introduced a barrier to force that situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request A: observes no key ─┐
                           ├─ Wait until both reads finish
Request B: observes no key ─┘

Then both execute their batches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inserts compete for the same composite primary key. Only one batch succeeds as a new creation; the other rereads the saved result and compares the fingerprint before returning it.&lt;/p&gt;

&lt;p&gt;Assertions verified that both initial lookups missed, only 1 note existed, and both calls returned the same body. The test did not infer concurrency safety merely from a run where one request happened to finish first.&lt;/p&gt;

&lt;p&gt;The initial SELECT can provide a fast replay path. The unique constraint and atomic writes prevent duplicate creation at the final boundary. An in-process Map alone would not cover requests handled by another process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Return to the same ID after losing a response
&lt;/h2&gt;

&lt;p&gt;The test threw an exception after creation committed to simulate a caller missing the success result. It did not interrupt actual HTTP traffic.&lt;/p&gt;

&lt;p&gt;Calling the function again with the same actor, key, and title returned the original body and status value &lt;code&gt;201&lt;/code&gt;, while the note count remained 1. Storing the result, rather than only a completion marker, lets the retried caller learn the same ID.&lt;/p&gt;

&lt;p&gt;The full set of cases was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response lost after commit, followed by identical retry&lt;/td&gt;
&lt;td&gt;Same body and 201; 1 note&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same actor and key, changed title&lt;/td&gt;
&lt;td&gt;Rejected with 409; still 1 note&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Another actor uses the same key&lt;/td&gt;
&lt;td&gt;Separate note; 2 total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both concurrent requests observe no key&lt;/td&gt;
&lt;td&gt;1 note, identical bodies, 1 creation and 1 replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Note creation violates a CHECK constraint&lt;/td&gt;
&lt;td&gt;0 notes and 0 saved results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saved result deleted before an identical retry&lt;/td&gt;
&lt;td&gt;New note; 2 total&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last case explicitly deleted the record to simulate the end of retention. Automatic TTL deletion was not implemented. If keys are not remembered forever, clients need to know what happens when the same request arrives after retention ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  This implementation caches only successful creation
&lt;/h2&gt;

&lt;p&gt;The environment was Node.js &lt;code&gt;v24.15.0&lt;/code&gt;, Wrangler &lt;code&gt;4.81.1&lt;/code&gt;, and Miniflare &lt;code&gt;4.20260409.0&lt;/code&gt;. Remote bindings were disabled; only local D1 and synthetic data were used.&lt;/p&gt;

&lt;p&gt;The implementation atomically saves the note and its successful result. It does not reproduce a contract such as Stripe's that also retains failure results after execution begins. Authentication, input validation, replay of HTTP headers and the complete response, and retention operations require separate work.&lt;/p&gt;

&lt;p&gt;The batch also contains only D1 writes. This mechanism alone cannot make an external notification or file transfer happen once. An external operation needs its own defined key and result-reuse behavior.&lt;/p&gt;

&lt;p&gt;If duplicate business data must remain impossible after request records expire, enforce that condition in the business table too. The time window for deduplicating retries need not match the lifetime of a business uniqueness rule.&lt;/p&gt;

&lt;p&gt;Review scope, input matching, concurrent creation, result replay, and post-deletion behavior alongside the presence of a key. Use those results to describe which duplicate operations the API prevents.&lt;/p&gt;

</description>
      <category>api</category>
      <category>cloudflare</category>
      <category>database</category>
      <category>testing</category>
    </item>
    <item>
      <title>Choosing Cron Triggers, Queues, or Workflows by where recovery should resume</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Mon, 05 Oct 2026 02:35:15 +0000</pubDate>
      <link>https://dev.to/hirodeath/choosing-cron-triggers-queues-or-workflows-by-where-recovery-should-resume-2616</link>
      <guid>https://dev.to/hirodeath/choosing-cron-triggers-queues-or-workflows-by-where-recovery-should-resume-2616</guid>
      <description>&lt;p&gt;A daily report, a call to an external API, and a process waiting for approval are all background work. Combining them under one mechanism can obscure what should be repeated after failure.&lt;/p&gt;

&lt;p&gt;Cron Triggers handle when work starts, Queues handle messages being delivered, and Workflows handle stages of a process with intermediate state. Choose by the unit of recovery as well as execution duration.&lt;/p&gt;

&lt;p&gt;Official documentation was checked on September 11, 2026. The reporting and approval flow below is a design example. No production triggers, queues, or workflows were created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Describe the unit of retry in one sentence
&lt;/h2&gt;

&lt;p&gt;Consider a daily participation report for several organizations. Separate the initial requirement from later additions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Desired recovery unit&lt;/th&gt;
&lt;th&gt;Mechanism to consider&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start a short report every morning&lt;/td&gt;
&lt;td&gt;The report for that day&lt;/td&gt;
&lt;td&gt;Cron Triggers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process organizations independently and retry only some&lt;/td&gt;
&lt;td&gt;A message identifying the organization and reporting date&lt;/td&gt;
&lt;td&gt;Queues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wait for review after reporting, then continue if approved&lt;/td&gt;
&lt;td&gt;The unfinished step of the overall process&lt;/td&gt;
&lt;td&gt;Workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are not mutually exclusive choices. A time trigger can start message delivery, with only the jobs that need it entering a multi-stage process.&lt;/p&gt;

&lt;p&gt;All three are not necessarily needed from the beginning. A short one-shot report requires different state from a process with per-organization delivery and approval waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cron Triggers take responsibility for when work starts
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developers.cloudflare.com/workers/configuration/cron-triggers/" rel="noopener noreferrer"&gt;Cron Triggers documentation&lt;/a&gt; describes invoking a Worker's scheduled handler using cron expressions, with times interpreted in UTC.&lt;/p&gt;

&lt;p&gt;For a daily report, decide the reporting date separately from the start time. If execution crosses a date boundary or a previous day's job is repeated, the intended reporting period should not change accidentally.&lt;/p&gt;

&lt;p&gt;The design could use a business job identifier like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;daily-summary / organization-red / 2026-09-11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a business identity example, not a trigger configuration. Decide whether rerunning the same date is the same job or a revised report with a new version.&lt;/p&gt;

&lt;p&gt;Installing a Cron Trigger does not itself represent “red finished, blue remains incomplete.” If a short batch remains sufficient, store those outcomes in a database or another record and define which portions can be repeated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queues define which message receives a retry
&lt;/h2&gt;

&lt;p&gt;When organizations should be processed independently, a message can become the delivery unit. Attach the needed parameters to the identity above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.cloudflare.com/queues/reference/delivery-guarantees/" rel="noopener noreferrer"&gt;Queues' delivery guarantee&lt;/a&gt; is at-least-once. A message may arrive more than once, so received-message counts and business-operation counts are different.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developers.cloudflare.com/queues/configuration/batching-retries/" rel="noopener noreferrer"&gt;ack and retry documentation&lt;/a&gt; describes acknowledging individual messages so that an ensuing batch failure does not redeliver those already acknowledged. Individual retry requests redelivery for that message. A successful consumer return also provides automatic acknowledgement.&lt;/p&gt;

&lt;p&gt;Suppose a batch contains jobs for red, blue, and green, but only blue encounters an external API failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;red:   saved successfully → ack
blue:  temporary failure → retry
green: saved successfully → ack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proposed handling records which jobs succeeded and returns the failed job for redelivery. However, if the API succeeded but its response was lost, a retry can still repeat an external side effect. Acknowledgement controls message delivery; it does not make the external operation happen exactly once.&lt;/p&gt;

&lt;p&gt;Decide where jobs go after retries are exhausted. If using a dead-letter queue, assign responsibility for inspecting and redriving it. This investigation did not create or execute a consumer or DLQ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflows represent completed stages and waiting
&lt;/h2&gt;

&lt;p&gt;A process that waits for an administrator after generating a report needs more than a standalone message. It needs to represent completed work, the condition being awaited, and what will resume execution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.cloudflare.com/workflows/build/sleeping-and-retrying/" rel="noopener noreferrer"&gt;Workflows' sleep and retry documentation&lt;/a&gt; describes steps, retries, and waits. The &lt;a href="https://developers.cloudflare.com/workflows/build/events-and-parameters/" rel="noopener noreferrer"&gt;event-waiting API&lt;/a&gt; also provides &lt;code&gt;waitForEvent()&lt;/code&gt; for a specified event type.&lt;/p&gt;

&lt;p&gt;A proposed flow could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Finalize the report for organization red and the reporting date
Save the result for review
Wait for an approval event
Validate the approval and continue
Record the outcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Putting report generation, external transmission, and a subsequent save into one step broadens the work repeated when the later operation fails. Step boundaries determine retry boundaries.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developers.cloudflare.com/workflows/build/rules-of-workflows/" rel="noopener noreferrer"&gt;Rules of Workflows&lt;/a&gt; also call for idempotent API and binding operations because a step can run repeatedly. Moving code into a Workflow does not automatically deduplicate external sends.&lt;/p&gt;

&lt;p&gt;An approval wait needs a deadline and an expiration policy. The end of a wait without approval must not be interpreted as approval. Authenticating the sender and checking that the event applies to the correct job remain application responsibilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve business identity when combining the mechanisms
&lt;/h2&gt;

&lt;p&gt;A possible architecture as requirements grow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cron: choose the reporting date and start
  ↓
Queues: deliver each organization's job
  ↓
Workflows: handle jobs needing waits or multiple stages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A record that the daily trigger ran does not reveal whether a particular organization's job is still awaiting approval. Conversely, a completed Workflow does not establish that jobs were created for every organization expected that day.&lt;/p&gt;

&lt;p&gt;Keep the reporting date, organization, and job ID connected, then compare the expected job set with completed jobs. Unconditionally creating new IDs on redelivery or restart makes duplicate business work harder to trace.&lt;/p&gt;

&lt;p&gt;This architecture also adds delivery configuration, step state, and logs to investigate. Throughput and cost were not compared, so this article does not claim that combining the mechanisms improves efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan partial failures before testing the rollout
&lt;/h2&gt;

&lt;p&gt;At an actual rollout, separately test at least an isolated organization failure, a lost response after a save, and an approval that never arrives. These are proposed checks for the target environment, not tests executed in this investigation.&lt;/p&gt;

&lt;p&gt;They examine delivery granularity, idempotency, and waiting-state deadlines respectively. Reducing all of them to “the batch failed” loses the information needed to choose a restart point.&lt;/p&gt;

&lt;p&gt;Start by defining the unit of work that can be repeated. A timed invocation may suffice; other work needs per-message redelivery or preserved progress while waiting for the next stage. Add components according to that distinction.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>architecture</category>
      <category>distributed</category>
      <category>backend</category>
    </item>
    <item>
      <title>Inventory cross-room queries before moving D1 data into Durable Objects</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Sun, 04 Oct 2026 03:01:29 +0000</pubDate>
      <link>https://dev.to/hirodeath/inventory-cross-room-queries-before-moving-d1-data-into-durable-objects-3oh3</link>
      <guid>https://dev.to/hirodeath/inventory-cross-room-queries-before-moving-d1-data-into-durable-objects-3oh3</guid>
      <description>&lt;p&gt;When moving room state into Durable Objects, similar storage SQL does not mean every D1 query can move unchanged. Each Object has its own SQLite storage. Splitting data previously held in one D1 database also changes how you retrieve records across rooms.&lt;/p&gt;

&lt;p&gt;This article tests that boundary. The experiment stored the same synthetic data in one local D1 database and two local Objects, then compared individual and cross-room reads. No live service was migrated. The migration steps later in the article are design proposals based on the experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a query the migration must preserve
&lt;/h2&gt;

&lt;p&gt;Consider a small room system: &lt;code&gt;alice&lt;/code&gt; belongs to &lt;code&gt;red&lt;/code&gt;, and &lt;code&gt;bob&lt;/code&gt; belongs to &lt;code&gt;blue&lt;/code&gt;. One D1 database can store both in this table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;room_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;room_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides listing members of a particular room, an administration screen can retrieve every membership with one query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;room_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;room_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This full-table read is for comparison. A large administration screen needs pagination, and reading every row may be undesirable. The point here is that a query spanning room boundaries stays within one database.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/" rel="noopener noreferrer"&gt;Durable Object Storage specification&lt;/a&gt; describes storage attached to an individual Object. It is not a shared database that automatically brings another Object's tables into this &lt;code&gt;SELECT&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the same table in two Objects
&lt;/h2&gt;

&lt;p&gt;The experimental &lt;code&gt;Room&lt;/code&gt; class creates a &lt;code&gt;member&lt;/code&gt; table during initialization and exposes RPC methods to add and retrieve members. Because the Object itself represents a room, this table has no &lt;code&gt;room_id&lt;/code&gt; column.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DurableObject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudflare:workers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Room&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DurableObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** @param {DurableObjectState} ctx @param {Env} env */&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blockConcurrencyWhile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CREATE TABLE IF NOT EXISTS member (user_id TEXT PRIMARY KEY)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cm"&gt;/** @param {string} userId */&lt;/span&gt;
  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO member (user_id) VALUES (?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;members&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT user_id FROM member ORDER BY user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Env&lt;/code&gt; is generated from the Wrangler binding configuration. Here, &lt;code&gt;blockConcurrencyWhile&lt;/code&gt; controls event delivery during initialization. This is not an example of putting a long external request inside that block; consult the &lt;a href="https://developers.cloudflare.com/durable-objects/api/state/" rel="noopener noreferrer"&gt;State API documentation&lt;/a&gt; when choosing where to use it.&lt;/p&gt;

&lt;p&gt;The caller obtains Objects by room name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;red&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ROOMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;room:red&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ROOMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;room:blue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;red&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the same name in the same namespace provides a way back to the same storage. The &lt;a href="https://developers.cloudflare.com/durable-objects/api/namespace/" rel="noopener noreferrer"&gt;Namespace API&lt;/a&gt; distinguishes named lookup from generating a new unique ID. Calling &lt;code&gt;newUniqueId()&lt;/code&gt; on every request does not look up an existing room.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the local run returned
&lt;/h2&gt;

&lt;p&gt;After completing the writes, the experiment performed the reads below. The harness passed 8 assertions, including the successful HTTP response.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;th&gt;Returned data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;members()&lt;/code&gt; on &lt;code&gt;room:red&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;alice&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;members()&lt;/code&gt; on &lt;code&gt;room:blue&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;bob&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieve again using the same name, &lt;code&gt;room:red&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;alice&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A separate Object created with &lt;code&gt;newUniqueId()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Empty array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global D1 query&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;blue/bob&lt;/code&gt; and &lt;code&gt;red/alice&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicitly fetch both rooms over RPC and combine&lt;/td&gt;
&lt;td&gt;Matches the global D1 query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fetch and combine with only &lt;code&gt;red&lt;/code&gt; in the target list&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;red/alice&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The application can still gather data after storage is split. This is the collection function used in the experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @param {Env} env @param {string[]} roomIds */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;collectMembers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;roomIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ROOMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`room:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;members&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;room_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room_id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller is responsible for the room list. Getting 1 record after passing &lt;code&gt;['red']&lt;/code&gt; does not establish that only 1 record exists globally. This experiment used a fixed room list; it implements neither enumeration of all Objects nor persistent management of that list.&lt;/p&gt;

&lt;p&gt;Waiting for each RPC and combining the arrays does not create a transaction that reads all Objects at the same instant. The results matched here with writes stopped. A consistent global snapshot under concurrent updates was not tested. If any RPC fails, this &lt;code&gt;Promise.all&lt;/code&gt; also fails. Whether to return partial results or repeat the entire read is a separate decision based on the screen's requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide where reads and writes will go before migrating
&lt;/h2&gt;

&lt;p&gt;Based on this result, an investigation of a proposed migration can inventory existing queries as follows. The right column lists decisions to make, not features implemented in this experiment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Existing operation&lt;/th&gt;
&lt;th&gt;Question after partitioning storage by Object&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retrieve members of one room&lt;/td&gt;
&lt;td&gt;Can the room ID consistently reach the same Object?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search all rooms in an administration screen&lt;/td&gt;
&lt;td&gt;Keep aggregate data elsewhere, or collect from a known target list?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find every room a user has joined&lt;/td&gt;
&lt;td&gt;Where will an index starting from the user live?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make a change affecting multiple rooms&lt;/td&gt;
&lt;td&gt;How will partial success be detected and recovered?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One possible design retains aggregate data in D1 for global search. That requires deciding which store owns updates, how much projection lag is acceptable, and what can rebuild missing updates. Sequential writes to D1 and an Object do not make the two stores one transaction. Writing the same data twice in this experiment prepared the comparison; it did not implement a synchronization mechanism.&lt;/p&gt;

&lt;p&gt;Another option keeps data in D1 and uses Objects to coordinate communication or operations for a room. Conversely, storing data inside the Object can be useful when state should stay within the room boundary. Apply the combination of execution context and storage described in the &lt;a href="https://developers.cloudflare.com/durable-objects/concepts/what-are-durable-objects/" rel="noopener noreferrer"&gt;Durable Objects overview&lt;/a&gt; to the unit of work in your application.&lt;/p&gt;

&lt;p&gt;Inventory read and write paths to identify which cross-room operations must change, even though both stores use SQLite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment and reproduction
&lt;/h2&gt;

&lt;p&gt;The experiment ran on September 11, 2026, with Node.js &lt;code&gt;v24.15.0&lt;/code&gt;, Wrangler &lt;code&gt;4.131.0&lt;/code&gt;, and its Miniflare dependency, &lt;code&gt;5.20260910.0-alpha&lt;/code&gt;. The Worker's compatibility date was &lt;code&gt;2026-09-11&lt;/code&gt;. The package's &lt;code&gt;convertV4MiniflareOptions&lt;/code&gt; converted the older option format. Wrangler generated the types, and &lt;code&gt;checkJs&lt;/code&gt; passed with &lt;code&gt;@cloudflare/workers-types&lt;/code&gt; &lt;code&gt;5.20260911.1&lt;/code&gt; and TypeScript &lt;code&gt;5.9.3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/do-partitions.mjs" rel="noopener noreferrer"&gt;local experiment script&lt;/a&gt; has its Worker and configuration in the adjacent &lt;code&gt;do-partitions/&lt;/code&gt; directory. The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-04/do-partitions.json" rel="noopener noreferrer"&gt;recorded output&lt;/a&gt; is also available.&lt;/p&gt;

&lt;p&gt;To reproduce it, install the specified Wrangler version in a separate scratch directory and pass the path to its &lt;code&gt;node_modules/wrangler/package.json&lt;/code&gt; as the script argument. It does not connect to remote D1 databases or Objects. Storage is temporary, and the runtime is disposed at the end.&lt;/p&gt;

&lt;p&gt;Performance, pricing, WebSockets, recovery across failures, and production data migration are outside this experiment. The results show the scope of reads after partitioning storage by Object and the application work needed for a global read.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>architecture</category>
      <category>database</category>
      <category>distributed</category>
    </item>
    <item>
      <title>Replacing deep OFFSET pagination in D1: rows read and inserts between pages</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Sat, 03 Oct 2026 02:30:51 +0000</pubDate>
      <link>https://dev.to/hirodeath/replacing-deep-offset-pagination-in-d1-rows-read-and-inserts-between-pages-2b5</link>
      <guid>https://dev.to/hirodeath/replacing-deep-offset-pagination-in-d1-rows-read-and-inserts-between-pages-2b5</guid>
      <description>&lt;p&gt;&lt;code&gt;LIMIT 20 OFFSET ...&lt;/code&gt; makes the next page of a list easy to express. But deeper pages require skipping more rows without returning them. Using an index does not, by itself, eliminate that work.&lt;/p&gt;

&lt;p&gt;This experiment inserted 100,000 synthetic notifications into local D1 and fetched the same 20 records with OFFSET and a cursor. It examined how many rows were read and what happened when a new row arrived between pages.&lt;/p&gt;

&lt;p&gt;The checks ran on September 11, 2026. They do not measure production D1 latency or billing, or establish a universal row count at which pagination becomes slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define a key that preserves the same order
&lt;/h2&gt;

&lt;p&gt;The experimental schema was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_feed&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It contained 100000 rows for &lt;code&gt;user-a&lt;/code&gt;, with &lt;code&gt;created_at&lt;/code&gt; set to the integer part of the ID divided by 3. Multiple records therefore shared a timestamp.&lt;/p&gt;

&lt;p&gt;The order was &lt;code&gt;created_at DESC, id DESC&lt;/code&gt;. Timestamp alone cannot uniquely order tied rows, so the ID belongs in both the ordering and the cursor. This example assumes non-null ordering columns and a unique ID.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.sqlite.org/rowvalue.html" rel="noopener noreferrer"&gt;SQLite's Row Values documentation&lt;/a&gt; describes comparing multiple columns together and applying that technique to scrolling lists. This experiment checked it in D1 SELECT statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieve the same 20 records with both methods
&lt;/h2&gt;

&lt;p&gt;The OFFSET query skips a specified number of records from the beginning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cursor query searches after the last record of the previous page. Because the order is descending, the comparison uses &lt;code&gt;&amp;lt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cursor values are the previous page's final &lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt;. Using only &lt;code&gt;created_at &amp;lt; ?&lt;/code&gt; could skip remaining rows sharing the boundary timestamp.&lt;/p&gt;

&lt;p&gt;The measured positions were offsets 20, 1000, 10000, 50000, and 99000. For each comparison, the immediately preceding row was retrieved in advance. Finding that cursor was excluded from the measurement. A real “Next” action carries those values from the previous result; jumping directly to an arbitrary page number is a different operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rows read differ substantially from rows returned
&lt;/h2&gt;

&lt;p&gt;At every position, the ID-and-timestamp arrays matched and contained 20 records. The final measured query's &lt;code&gt;meta.rows_read&lt;/code&gt; values were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OFFSET position&lt;/th&gt;
&lt;th&gt;Rows read with OFFSET&lt;/th&gt;
&lt;th&gt;Rows read with cursor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1020&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;10020&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;td&gt;50020&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99000&lt;/td&gt;
&lt;td&gt;99020&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cursor did not always read exactly 20 rows; it read 21 to 23 in this dataset. OFFSET's reads increased with the position.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPLAIN QUERY PLAN&lt;/code&gt; showed both queries using the same covering index. The relevant output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OFFSET:
SEARCH notification USING COVERING INDEX idx_feed (user_id=?)

Cursor:
SEARCH notification USING COVERING INDEX idx_feed (user_id=? AND created_at&amp;lt;?)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check which condition defines the search starting point as well as whether an index is used. Equality of the returned arrays also verified that the comparison including &lt;code&gt;id&lt;/code&gt; produced the intended results.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developers.cloudflare.com/d1/best-practices/use-indexes/" rel="noopener noreferrer"&gt;D1 index documentation&lt;/a&gt; distinguishes returned rows from read rows and describes inspection using &lt;code&gt;EXPLAIN QUERY PLAN&lt;/code&gt; and &lt;code&gt;meta.rows_read&lt;/code&gt;. These numbers came from local Miniflare; they are not production-billing measurements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timing does not establish one universal cutoff
&lt;/h2&gt;

&lt;p&gt;The environment was macOS/arm64 on an Apple M4 Max, Node.js &lt;code&gt;v24.15.0&lt;/code&gt;, Wrangler &lt;code&gt;4.81.1&lt;/code&gt;, and Miniflare &lt;code&gt;4.20260409.0&lt;/code&gt;. The configuration specified compatibility date &lt;code&gt;2026-09-11&lt;/code&gt;, but this version of &lt;code&gt;getPlatformProxy&lt;/code&gt; does not pass that date to the Worker it creates. The measurements cover this local D1 implementation, not Worker behavior for the requested date. Remote bindings were disabled and local D1 persistence was off.&lt;/p&gt;

&lt;p&gt;Each position had 4 warmup rounds followed by 11 measured rounds. Query order alternated between OFFSET and cursor. &lt;code&gt;performance.now()&lt;/code&gt; measured the wait for &lt;code&gt;.all()&lt;/code&gt;, including local proxy overhead but excluding cursor preparation and result assertions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OFFSET position&lt;/th&gt;
&lt;th&gt;OFFSET median ms&lt;/th&gt;
&lt;th&gt;Cursor median ms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;1.435&lt;/td&gt;
&lt;td&gt;1.219&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1.113&lt;/td&gt;
&lt;td&gt;0.990&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;1.026&lt;/td&gt;
&lt;td&gt;1.084&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;td&gt;1.809&lt;/td&gt;
&lt;td&gt;1.123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99000&lt;/td&gt;
&lt;td&gt;2.971&lt;/td&gt;
&lt;td&gt;1.137&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At shallow positions, call-to-call variation matters; the medians even reversed at 10000. The cursor at the first position had a maximum observation of 9.911 ms. This table does not justify a universal rule such as switching at 10000 records.&lt;/p&gt;

&lt;p&gt;The clearer finding is that deeper OFFSET queries read more rows, with timing differences also appearing at the deeper positions in this run. A production decision needs measurements against the real distribution, page size, query frequency, and response-time target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inserting between pages produces an OFFSET overlap
&lt;/h2&gt;

&lt;p&gt;The test also fetched the first 20 records, then inserted 1 new row ahead of them.&lt;/p&gt;

&lt;p&gt;The first page ended at ID &lt;code&gt;99981&lt;/code&gt;. Fetching the next page with &lt;code&gt;OFFSET 20&lt;/code&gt; after the insertion returned &lt;code&gt;99981&lt;/code&gt; again as its first row. The new leading record pushed the previous twentieth record into the twenty-first position.&lt;/p&gt;

&lt;p&gt;Using the saved cursor instead returned &lt;code&gt;99980&lt;/code&gt; first, with no IDs overlapping the previous page. Assertions checked that result too.&lt;/p&gt;

&lt;p&gt;A cursor is not a snapshot of the entire list, however. Updating ordering values between requests, or reproducing the same list including deleted records, needs additional rules. The mutation test here covered only 1 insertion at the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with lists that need a Next action
&lt;/h2&gt;

&lt;p&gt;Cursor pagination receives its next position from the preceding page. An interface that jumps directly to a page number must locate that position separately. Excluding cursor preparation from this experiment's timing makes that distinction explicit.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/d1-pagination.mjs" rel="noopener noreferrer"&gt;reproduction code&lt;/a&gt; includes data creation, result equality, query plans, measurements, and the insertion test. Alongside index usage, check that the methods return the same records and how much each reads to reach the next page.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>Debugging mixed ESM and CommonJS: separate module classification from loading</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Fri, 02 Oct 2026 02:44:50 +0000</pubDate>
      <link>https://dev.to/hirodeath/debugging-mixed-esm-and-commonjs-separate-module-classification-from-loading-3072</link>
      <guid>https://dev.to/hirodeath/debugging-mixed-esm-and-commonjs-separate-module-classification-from-loading-3072</guid>
      <description>&lt;p&gt;You see &lt;code&gt;require is not defined&lt;/code&gt;, replace it with an import, and then encounter a syntax error in another file. When ESM and CommonJS coexist, changing syntax alone can make the original cause difficult to follow.&lt;/p&gt;

&lt;p&gt;First ask how Node.js classifies the file. Then ask how that file loads another module. Separating these questions helps determine whether to change &lt;code&gt;package.json&lt;/code&gt;, a filename extension, or the asynchronous boundary of the caller.&lt;/p&gt;

&lt;p&gt;This investigation ran 10 small-file cases in separate processes on Node.js &lt;code&gt;v24.15.0&lt;/code&gt;. It used no bundler, TypeScript transformation, or custom loader, and removed &lt;code&gt;NODE_OPTIONS&lt;/code&gt; from child processes. Official documentation was checked on September 11, 2026; the latest pages displayed &lt;code&gt;v26.8.2&lt;/code&gt;. The experimental results are specifically for &lt;code&gt;v24.15.0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the extension and nearest package.json first
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://nodejs.org/api/packages.html" rel="noopener noreferrer"&gt;official Packages documentation&lt;/a&gt; identifies &lt;code&gt;.mjs&lt;/code&gt; as ESM and &lt;code&gt;.cjs&lt;/code&gt; as CommonJS. For &lt;code&gt;.js&lt;/code&gt;, the nearest parent &lt;code&gt;package.json&lt;/code&gt; can explicitly select the system through &lt;code&gt;type&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;“Nearest” matters: inspecting only the repository root can miss the applicable setting. The experiment placed a child directory with &lt;code&gt;type: commonjs&lt;/code&gt; inside one configured with &lt;code&gt;type: module&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;esm/
  package.json            type: module
  require.js
  legacy.cjs
  nested/
    package.json          type: commonjs
    legacy.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;require()&lt;/code&gt; inside &lt;code&gt;require.js&lt;/code&gt; failed. In both &lt;code&gt;legacy.cjs&lt;/code&gt; and &lt;code&gt;nested/legacy.js&lt;/code&gt;, however, &lt;code&gt;typeof require&lt;/code&gt; produced &lt;code&gt;function&lt;/code&gt;. The extension or the inner configuration can select CommonJS even inside an outer ESM package area.&lt;/p&gt;

&lt;p&gt;The reverse was checked too. A static &lt;code&gt;export&lt;/code&gt; in a &lt;code&gt;.js&lt;/code&gt; file under &lt;code&gt;type: commonjs&lt;/code&gt; produced a syntax error, while an &lt;code&gt;.mjs&lt;/code&gt; file in the same area executed successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  An untyped js file is not necessarily CommonJS
&lt;/h2&gt;

&lt;p&gt;Assuming that omitting &lt;code&gt;type&lt;/code&gt; always preserves the old CommonJS behavior is incomplete for current Node.js. The documentation describes syntax detection: input without an explicit classification can be treated as ESM when it contains ESM-only syntax.&lt;/p&gt;

&lt;p&gt;The experiment ran this &lt;code&gt;.js&lt;/code&gt; file in a directory containing an empty &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It printed &lt;code&gt;42&lt;/code&gt; and emitted a &lt;code&gt;MODULE_TYPELESS_PACKAGE_JSON&lt;/code&gt; warning. The same &lt;code&gt;export&lt;/code&gt; failed with an explicit &lt;code&gt;type: commonjs&lt;/code&gt;, but succeeded through syntax detection when &lt;code&gt;type&lt;/code&gt; was absent.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;type: module&lt;/code&gt; simply to remove that warning also affects existing &lt;code&gt;.js&lt;/code&gt; files governed by the same setting. First identify the target file and the files sharing that configuration. For a localized migration, &lt;code&gt;.mjs&lt;/code&gt; and &lt;code&gt;.cjs&lt;/code&gt; can make the intention explicit.&lt;/p&gt;

&lt;p&gt;The performance cost of syntax detection was not measured. This experiment checked execution outcomes and the warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  require can load some ES modules
&lt;/h2&gt;

&lt;p&gt;The older rule “CommonJS must always use &lt;code&gt;import()&lt;/code&gt; to load ESM” also needs qualification. The &lt;a href="https://nodejs.org/download/release/v24.15.0/docs/api/modules.html" rel="noopener noreferrer"&gt;CommonJS documentation for the tested version&lt;/a&gt; describes the conditions under which &lt;code&gt;require()&lt;/code&gt; can load synchronous ESM.&lt;/p&gt;

&lt;p&gt;The target ES module contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CommonJS caller read the named export from the returned object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../esm/sync.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It printed &lt;code&gt;42&lt;/code&gt;. An &lt;code&gt;.mjs&lt;/code&gt; extension alone does not establish that &lt;code&gt;require()&lt;/code&gt; must reject the module.&lt;/p&gt;

&lt;p&gt;Changing the target to this made &lt;code&gt;require()&lt;/code&gt; fail with &lt;code&gt;ERR_REQUIRE_ASYNC_MODULE&lt;/code&gt; in the same environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is top-level await. The documentation also excludes synchronous loading when top-level await occurs elsewhere in the imported module graph. The experiment placed it directly in the target module.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic import does not turn its caller into ESM
&lt;/h2&gt;

&lt;p&gt;The CommonJS file then loaded the asynchronous ES module like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../esm/async.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was &lt;code&gt;function 42&lt;/code&gt;. It waited for the ES module and obtained its value, while &lt;code&gt;require&lt;/code&gt; remained available in the caller. Adding an &lt;code&gt;import()&lt;/code&gt; expression did not switch the entire CommonJS file into ESM.&lt;/p&gt;

&lt;p&gt;However, replacing a synchronous load with &lt;code&gt;import()&lt;/code&gt; means waiting for its result. The caller may also need an asynchronous interface. Distinguish a configuration problem from a change that requires an asynchronous boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correct classification does not guarantee correct path resolution
&lt;/h2&gt;

&lt;p&gt;An ESM relative import of &lt;code&gt;./sync&lt;/code&gt; failed with &lt;code&gt;ERR_MODULE_NOT_FOUND&lt;/code&gt; even though &lt;code&gt;sync.mjs&lt;/code&gt; was next to it. The &lt;a href="https://nodejs.org/api/esm.html" rel="noopener noreferrer"&gt;ESM documentation&lt;/a&gt; requires extensions for relative and absolute import specifiers.&lt;/p&gt;

&lt;p&gt;That is separate from whether the target is ESM or CommonJS. This example needs &lt;code&gt;./sync.mjs&lt;/code&gt;. A shorthand accepted by a bundler need not work when passed directly to Node.js.&lt;/p&gt;

&lt;p&gt;The 10 checked cases were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;require in a &lt;code&gt;.js&lt;/code&gt; file under &lt;code&gt;type: module&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;require is not defined&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static export in &lt;code&gt;.js&lt;/code&gt; under &lt;code&gt;type: commonjs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Syntax error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.cjs&lt;/code&gt; under &lt;code&gt;type: module&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Runs as CommonJS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.mjs&lt;/code&gt; under &lt;code&gt;type: commonjs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Runs as ESM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.js&lt;/code&gt; beneath an inner &lt;code&gt;type: commonjs&lt;/code&gt; package&lt;/td&gt;
&lt;td&gt;Uses the inner setting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CommonJS requires synchronous ESM&lt;/td&gt;
&lt;td&gt;Retrieves the value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CommonJS requires ESM with top-level await&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ERR_REQUIRE_ASYNC_MODULE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CommonJS dynamically imports that ESM&lt;/td&gt;
&lt;td&gt;Retrieves the value; require remains available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An ESM relative import omits the extension&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ERR_MODULE_NOT_FOUND&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A &lt;code&gt;.js&lt;/code&gt; file with no type contains export&lt;/td&gt;
&lt;td&gt;Runs through syntax detection, with a warning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/node-module-boundaries.mjs" rel="noopener noreferrer"&gt;reproduction script&lt;/a&gt; creates a temporary directory, asserts exit codes and output for every case, then removes it. It does not change an existing project's &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conditional package &lt;code&gt;exports&lt;/code&gt;, CommonJS named-export detection, and TypeScript configuration are outside this experiment. For an initial investigation of mixed-module errors, check the Node.js version, file classification, loading method, and relative path in that order.&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>debugging</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
