{"record":{"id":"7f114a32f74b9b29","repo":"clockworklabs/SpacetimeDB","slug":"fromcounterv7-uuid-counter-must-be-non-negativ","errorCode":null,"errorMessage":"`fromCounterV7` uuid `counter` must be non-negative","messagePattern":"`fromCounterV7` uuid `counter` must be non-negative","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/bindings-typescript/src/lib/uuid.ts","lineNumber":163,"sourceCode":"   *\n   * const uuid = Uuid.fromCounterV7(counter, now, randomBytes);\n   *\n   * console.assert(\n   *   uuid.toString() === \"0000647e-5180-7000-8000-000200000000\"\n   * );\n   * ```\n   */\n  static fromCounterV7(\n    counter: { value: number },\n    now: Timestamp,\n    randomBytes: Uint8Array\n  ): Uuid {\n    if (randomBytes.length !== 4) {\n      throw new Error('`fromCounterV7` requires `randomBytes.length == 4`');\n    }\n\n    if (counter.value < 0) {\n      throw new Error('`fromCounterV7` uuid `counter` must be non-negative');\n    }\n\n    if (now.__timestamp_micros_since_unix_epoch__ < 0) {\n      throw new Error('`fromCounterV7` `timestamp` before unix epoch');\n    }\n\n    // 31-bit monotonic counter with wraparound\n    const counterVal = counter.value;\n    counter.value = (counterVal + 1) & 0x7fff_ffff;\n\n    // 48-bit unix timestamp (ms)\n    const tsMs = now.toMillis() & 0xffff_ffff_ffffn;\n\n    const bytes = new Uint8Array(16);\n\n    // unix_ts_ms (48 bits)\n    bytes[0] = Number((tsMs >> 40n) & 0xffn);\n    bytes[1] = Number((tsMs >> 32n) & 0xffn);","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/bindings-typescript/src/lib/uuid.ts#L145-L181","documentation":"Uuid.fromCounterV7 expects a mutable counter object { value: number } holding a 31-bit non-negative count. The value is embedded into the UUID and then incremented with wraparound ((value + 1) & 0x7fffffff). A negative value has no representation in this scheme, so it is rejected up front.","triggerScenarios":"Passing { value: -1 } or any negative counter to Uuid.fromCounterV7(counter, now, randomBytes); a counter that underflowed through decrement logic or was loaded from a source that permits negatives.","commonSituations":"Initializing the counter to -1 as an 'unset' sentinel; decrementing the counter on some teardown path; deserializing a persisted counter that stored a signed value.","solutions":["Initialize the counter at 0 and only ever let fromCounterV7 increment it","Clamp before calling: counter.value = Math.max(0, counter.value)","Validate counter.value >= 0 at the boundary if the value comes from storage or the network"],"exampleFix":"// before\nconst counter = { value: -1 }; // 'unset' sentinel\nconst uuid = Uuid.fromCounterV7(counter, now, rand); // throws\n\n// after\nconst counter = { value: 0 };\nconst uuid = Uuid.fromCounterV7(counter, now, rand);","handlingStrategy":"validation","validationCode":"if (counter.value < 0) counter.value = 0; // or reject the input\nconst uuid = Uuid.fromCounterV7(counter, now, rand);","typeGuard":"function isNonNegativeCounter(c: unknown): c is { value: number } {\n  return (\n    typeof c === 'object' &&\n    c !== null &&\n    typeof (c as { value?: unknown }).value === 'number' &&\n    (c as { value: number }).value >= 0\n  );\n}","tryCatchPattern":null,"preventionTips":["Initialize counters at 0; never use -1 as a sentinel","Let fromCounterV7 be the only code that mutates the counter (it increments with wraparound)"],"tags":["uuid","typescript","counter","validation","uuidv7"],"backgroundTag":"argument-validation","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}