clockworklabs/SpacetimeDB · error · Error

`fromCounterV7` uuid `counter` must be non-negative

Error message

`fromCounterV7` uuid `counter` must be non-negative

What it means

fromCounterV7 embeds counter.value into a 31-bit unsigned field of the UUID, so negative values cannot be encoded and are rejected up front. The counter argument is a mutable box: after validation the method increments value and wraps it at 0x7fff_ffff, so callers read the next counter value back out of the same object.

Solutions

  1. Initialize the counter object to 0 (const counter = { value: 0 }) and keep it non-negative
  2. Sanitize on load: counter.value = Math.max(0, counter.value)
  3. Validate counter.value >= 0 before calling fromCounterV7 and surface your own config error

Example fix

// before
const counter = { value: -1 }; // sentinel
const id = Uuid.fromCounterV7(counter, now, rand); // throws

// after
const counter = { value: 0 };
const id = Uuid.fromCounterV7(counter, now, rand);
Defensive patterns

Strategy: validation

Validate before calling

if (!(counter.value >= 0)) {
  counter.value = 0; // or throw your own config error
}
const id = Uuid.fromCounterV7(counter, now, randomBytes);

Prevention

When it happens

Trigger: Calling fromCounterV7 with counter.value < 0, typically a counter initialized to -1 as a 'not yet set' sentinel, or a value loaded from a signed int32 source without sanitizing.

Common situations: C-style sentinel initialization (-1) carried into TypeScript; counters persisted to JSON or a signed DB column and restored negative; counters shared across modules with different signedness assumptions.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/7f114a32f74b9b29. Report an issue: GitHub.

Appendix: source

Thrown at crates/bindings-typescript/src/lib/uuid.ts:163

   *
   * const uuid = Uuid.fromCounterV7(counter, now, randomBytes);
   *
   * console.assert(
   *   uuid.toString() === "0000647e-5180-7000-8000-000200000000"
   * );
   * ```
   */
  static fromCounterV7(
    counter: { value: number },
    now: Timestamp,
    randomBytes: Uint8Array
  ): Uuid {
    if (randomBytes.length !== 4) {
      throw new Error('`fromCounterV7` requires `randomBytes.length == 4`');
    }

    if (counter.value < 0) {
      throw new Error('`fromCounterV7` uuid `counter` must be non-negative');
    }

    if (now.__timestamp_micros_since_unix_epoch__ < 0) {
      throw new Error('`fromCounterV7` `timestamp` before unix epoch');
    }

    // 31-bit monotonic counter with wraparound
    const counterVal = counter.value;
    counter.value = (counterVal + 1) & 0x7fff_ffff;

    // 48-bit unix timestamp (ms)
    const tsMs = now.toMillis() & 0xffff_ffff_ffffn;

    const bytes = new Uint8Array(16);

    // unix_ts_ms (48 bits)
    bytes[0] = Number((tsMs >> 40n) & 0xffn);
    bytes[1] = Number((tsMs >> 32n) & 0xffn);

View on GitHub (pinned to 6dee26c6ef)