{"record":{"id":"c78e01f482b1f647","repo":"thedotmack/claude-mem","slug":"projection-error-user-id-must-be-non-empty","errorCode":null,"errorMessage":"projection_error: user_id must be non-empty","messagePattern":"projection_error: user_id must be non-empty","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workers/sync-hub/src/do/SyncHub.ts","lineNumber":677,"sourceCode":"\t\t\tacquired: true,\n\t\t\tlease_token: token,\n\t\t\tepoch: this.meta(\"epoch\"),\n\t\t\thead_seq: head,\n\t\t\tprojected_seq: projected,\n\t\t\ttarget_seq: target,\n\t\t};\n\t}\n\n\tgetProjectionPage(\n\t\tleaseToken: string,\n\t\ttargetSeq: string,\n\t\tuserId: string,\n\t\tmaxOps = PROJECTION_PAGE_MAX_OPS,\n\t\tmaxBytes = PROJECTION_PAGE_MAX_BYTES,\n\t\tnow = Date.now(),\n\t): ProjectionPage {\n\t\tthis.assertLease(leaseToken, now);\n\t\tif (typeof userId !== \"string\" || userId.length === 0) throw projectionError(\"user_id must be non-empty\");\n\t\tconst target = assertCanonicalDecimal(targetSeq);\n\t\tconst projected = this.projectedSeq();\n\t\tconst epoch = this.meta(\"epoch\");\n\t\tif (compareCanonicalDecimals(target, this.headSeq()) > 0) throw projectionError(\"target_seq exceeds head_seq\");\n\t\tconst limit = Math.min(PROJECTION_PAGE_MAX_OPS, Math.max(1, Math.floor(maxOps)));\n\t\tconst byteLimit = Math.min(PROJECTION_PAGE_MAX_BYTES, Math.max(1, Math.floor(maxBytes)));\n\t\tconst rows = this.ctx.storage.sql.exec<{\n\t\t\tseq: string;\n\t\t\tbody: string;\n\t\t\toperation_sha256: string;\n\t\t\tserver_ts: string;\n\t\t}>(\n\t\t\t`SELECT seq, body, operation_sha256, server_ts\n\t\t\t FROM canonical_ops\n\t\t\t WHERE (LENGTH(seq) > LENGTH(?) OR (LENGTH(seq) = LENGTH(?) AND seq > ?))\n\t\t\t   AND (LENGTH(seq) < LENGTH(?) OR (LENGTH(seq) = LENGTH(?) AND seq <= ?))\n\t\t\t ORDER BY LENGTH(seq), seq LIMIT ?`,\n\t\t\tprojected,","sourceCodeStart":659,"sourceCodeEnd":695,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/do/SyncHub.ts#L659-L695","documentation":"getProjectionPage() validates that userId is a non-empty string before paging canonical ops for that user's projection. An empty or non-string userId cannot identify any projection target, so the DO throws this projectionError immediately after the lease check.","triggerScenarios":"Calling getProjectionPage(token, targetSeq, '') or passing undefined/null/a non-string as userId — e.g. userId omitted from a job payload, or a decode step producing an empty string for a missing field.","commonSituations":"Projection worker consuming queue messages where the user_id attribute was never set; JSON payloads with \"userId\": \"\" from an upstream producer bug; TS types claiming string while runtime data is empty after a schema change.","solutions":["Validate typeof userId === 'string' && userId.length > 0 before invoking the DO; fail the job with a clear producer-side error.","Fix the upstream producer to always stamp user_id into projection job payloads.","If userId comes from auth, check the token/session resolution succeeded before enqueueing projection work.","At runtime, coerce with a guard: if (!userId) skip/reject the job rather than calling the hub."],"exampleFix":"// before\nawait hub.getProjectionPage(lease.lease_token, target, job.userId);\n// after\nif (typeof job.userId !== 'string' || job.userId.length === 0) {\n  throw new Error(`projection job ${job.id} missing userId`);\n}\nawait hub.getProjectionPage(lease.lease_token, target, job.userId);","handlingStrategy":"validation","validationCode":"if (typeof userId !== 'string' || userId.length === 0) {\n  throw new TypeError('projection job requires non-empty userId');\n}","typeGuard":"function hasUserId(job: unknown): job is { userId: string } & Record<string, unknown> {\n  return typeof job === 'object' && job !== null &&\n    typeof (job as any).userId === 'string' && (job as any).userId.length > 0;\n}","tryCatchPattern":"try {\n  page = hub.getProjectionPage(token, target, userId);\n} catch (e) {\n  if (String(e).includes('user_id must be non-empty')) {\n    deadLetter(job, 'missing userId'); return; // don't retry producer bugs\n  }\n  throw e;\n}","preventionTips":["Validate job payloads at the queue consumer boundary","Stamp user_id at production time, assert on enqueue","Never rely on TS types alone for wire-format data"],"tags":["projection","validation","sync"],"backgroundTag":"empty-required-field","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}