@byollm/server 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/chunk-7RKXFPBZ.js +72 -0
- package/dist/chunk-7RKXFPBZ.js.map +1 -0
- package/dist/chunk-HL6EYHQ7.js +422 -0
- package/dist/chunk-HL6EYHQ7.js.map +1 -0
- package/dist/delivery-36nIe-b3.d.ts +73 -0
- package/dist/handlers-D7lWfwno.d.ts +52 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.js +634 -0
- package/dist/index.js.map +1 -0
- package/dist/next.d.ts +33 -0
- package/dist/next.js +17 -0
- package/dist/next.js.map +1 -0
- package/dist/store-D23N6iiP.d.ts +255 -0
- package/dist/supabase/index.d.ts +30 -0
- package/dist/supabase/index.js +414 -0
- package/dist/supabase/index.js.map +1 -0
- package/package.json +52 -0
- package/supabase/migrations/20260809000000_byollm_runner.sql +507 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/app.ts","../src/memory.ts"],"sourcesContent":["import {\n backendDescriptor,\n matchAudience,\n type DeliveredResult,\n type JobKind,\n} from \"@byollm/protocol\";\nimport {\n PollingDelivery,\n type PollingDeliveryDeps,\n type ResultDelivery,\n type WaitOptions,\n} from \"./delivery.js\";\nimport { generateRunnerId, generateRunnerToken, hashSecret } from \"./ids.js\";\nimport type { EnqueueInput, JobRecord, RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/**\n * How long since a runner's last heartbeat before it stops counting as live.\n * Three heartbeats of slack at the daemon's ~10s cadence.\n */\nconst DEFAULT_LIVENESS_MS = 35_000;\n\n/** Why a job cannot presently run. */\nexport type NoRunnerReason =\n | \"no-runner-paired\"\n | \"no-runner-online\"\n | \"no-matching-capability\"\n | \"audience-admits-nobody\";\n\n/**\n * The no-runner signal (byollm_001 Rev 1 §D).\n *\n * `available: false` means an app should fall back — hosted model, \"start\n * your runner\" prompt — rather than awaiting something that will never\n * resolve. A job still blocked on dependencies is **not** unavailable; it is\n * waiting, and saying otherwise would make every multi-job flow look broken\n * ({@link MUSTS.NO_RUNNER_SIGNAL}).\n */\nexport interface RunnerAvailability {\n readonly available: boolean;\n readonly reason?: NoRunnerReason;\n /** Live runners that could take work of this shape. */\n readonly candidates: number;\n}\n\nexport interface AvailabilityQuery {\n readonly kind: JobKind;\n readonly owner: string;\n readonly audience?: \"self\" | \"named\" | \"public\";\n readonly audienceAllow?: readonly string[];\n}\n\nexport interface ByollmAppOptions {\n readonly store: ByollmStore;\n /** Injectable clock. */\n readonly now?: () => number;\n /** Liveness window for the no-runner signal. */\n readonly livenessMs?: number;\n /**\n * How the app learns a job finished. Defaults to polling the store, which\n * is correct everywhere; the Supabase adapter substitutes Realtime.\n */\n readonly delivery?: (deps: PollingDeliveryDeps) => ResultDelivery;\n /**\n * How long a sustained no-runner signal must persist before `result()`\n * gives up. Longer tolerates a daemon restarting; shorter fails faster.\n */\n readonly noRunnerGraceMs?: number;\n}\n\n/**\n * An enqueued job, with the delivery channel attached.\n *\n * `result()` is sugar over the channel — with a timeout and a\n * `noRunnerAvailable` path — never a bare promise that can hang forever\n * (byollm_003 Rev 1).\n */\nexport interface JobHandle {\n readonly id: string;\n /** The job as stored at enqueue time. */\n readonly record: JobRecord;\n /** Wait for a terminal outcome. */\n result(options?: WaitOptions): Promise<DeliveredResult>;\n /** Ask the runner to stop. */\n cancel(): Promise<void>;\n}\n\n/**\n * The app-facing half of `@byollm/server`.\n *\n * The daemon talks to {@link ByollmHandlers}; the app talks to this. Keeping\n * them separate is what makes \"one door per state write\" hold — an app\n * enqueues and cancels through these methods and never writes job rows by\n * hand.\n */\nexport class ByollmApp {\n readonly #store: ByollmStore;\n readonly #now: () => number;\n readonly #livenessMs: number;\n readonly #delivery: ResultDelivery;\n\n constructor(options: ByollmAppOptions) {\n this.#store = options.store;\n this.#now = options.now ?? Date.now;\n this.#livenessMs = options.livenessMs ?? DEFAULT_LIVENESS_MS;\n\n const deps: PollingDeliveryDeps = {\n ...(options.noRunnerGraceMs === undefined\n ? {}\n : { graceMs: options.noRunnerGraceMs }),\n read: (jobId) => this.result(jobId),\n availability: async (jobId) => {\n const job = await this.#store.get(jobId);\n if (!job)\n return { available: false, reason: \"unknown-job\", blocked: false };\n // A job waiting on a dependency is waiting, not unavailable.\n if (job.claimableAt === null) {\n return { available: true, blocked: true };\n }\n const availability = await this.runnerAvailability({\n kind: job.kind,\n owner: job.owner,\n audience: job.audience,\n ...(job.audienceAllow === undefined\n ? {}\n : { audienceAllow: job.audienceAllow }),\n });\n return {\n available: availability.available,\n ...(availability.reason === undefined\n ? {}\n : { reason: availability.reason }),\n blocked: false,\n };\n },\n };\n this.#delivery = options.delivery?.(deps) ?? new PollingDelivery(deps);\n }\n\n /**\n * Enqueue a job.\n *\n * `audience` defaults to `self` — the safe direction. Widening it means the\n * result comes back marked untrusted (see {@link ByollmApp.result}), and\n * the app is obliged to disclose that to whoever reads it.\n */\n async enqueue(input: EnqueueInput): Promise<JobHandle> {\n const record = await this.#store.create(input, this.#now());\n return {\n id: record.id,\n record,\n result: (options?: WaitOptions) =>\n this.#delivery.waitFor(record.id, options),\n cancel: async () => {\n await this.cancel(record.id);\n },\n };\n }\n\n /** Read a job's current state. */\n async job(jobId: string): Promise<JobRecord | null> {\n await this.#store.expireDue(this.#now());\n return this.#store.get(jobId);\n }\n\n /**\n * A job's result with its provenance attached.\n *\n * Check `provenance.untrusted` before rendering. It is true for every\n * `named`/`public` job, because that text came from someone else's machine\n * and the app must not present it as its own AI's answer\n * ({@link MUSTS.RESULT_PROVENANCE}).\n */\n async result(jobId: string): Promise<DeliveredResult | null> {\n const job = await this.job(jobId);\n if (!job) return null;\n return {\n jobId: job.id,\n state: job.state,\n ...(job.outcome === null ? {} : { outcome: job.outcome }),\n ...(job.provenance === null ? {} : { provenance: job.provenance }),\n };\n }\n\n /** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */\n async cancel(jobId: string): Promise<JobRecord | null> {\n return this.#store.cancel(jobId, this.#now());\n }\n\n /**\n * Is there a live runner that could take a job of this shape?\n *\n * Runs the identical {@link matchAudience} rule the claim path uses, so the\n * signal cannot promise a runner the claim would then refuse.\n */\n async runnerAvailability(\n query: AvailabilityQuery,\n ): Promise<RunnerAvailability> {\n const now = this.#now();\n const all = await this.#store.listRunners();\n const live = all.filter(\n (runner) =>\n runner.revokedAt === null &&\n !runner.paused &&\n now - runner.lastHeartbeatAt <= this.#livenessMs,\n );\n\n if (all.length === 0) {\n return { available: false, reason: \"no-runner-paired\", candidates: 0 };\n }\n if (live.length === 0) {\n return { available: false, reason: \"no-runner-online\", candidates: 0 };\n }\n\n let capable = 0;\n let admitted = 0;\n for (const runner of live) {\n const capability = runner.capabilities.find((c) => c.kind === query.kind);\n if (!capability) continue;\n capable += 1;\n\n const match = matchAudience(\n {\n owner: query.owner,\n audience: query.audience ?? \"self\",\n audienceAllow: query.audienceAllow,\n },\n {\n owner: runner.owner,\n offerScope: capability.offerScope,\n account: backendDescriptor(capability.backendId).account,\n // Same conservative assumption the claim path makes: the server\n // cannot see a remote daemon's local allowlist (protocol §4.2).\n locallyAllows: () => true,\n },\n );\n if (match.ok) admitted += 1;\n }\n\n if (capable === 0) {\n return {\n available: false,\n reason: \"no-matching-capability\",\n candidates: 0,\n };\n }\n if (admitted === 0) {\n return {\n available: false,\n reason: \"audience-admits-nobody\",\n candidates: 0,\n };\n }\n return { available: true, candidates: admitted };\n }\n\n /**\n * Approve a pairing on behalf of an authenticated user.\n *\n * `owner` MUST come from the approving user's own session. A daemon can\n * never assert who it is — that is the whole reason pairing is interactive\n * ({@link MUSTS.PAIR_ONE_USER}, {@link MUSTS.PAIR_INTERACTIVE}).\n */\n async approvePairing(args: {\n userCode: string;\n owner: string;\n }): Promise<RunnerRecord> {\n const token = generateRunnerToken();\n return this.#store.approvePairing({\n userCode: normalizeUserCode(args.userCode),\n owner: args.owner,\n runnerId: generateRunnerId(),\n runnerToken: token,\n tokenHash: hashSecret(token),\n now: this.#now(),\n });\n }\n\n /** Deny a pairing the user did not initiate. */\n async denyPairing(userCode: string): Promise<void> {\n return this.#store.denyPairing(normalizeUserCode(userCode), this.#now());\n }\n\n /** What a pairing code refers to, for the approval page to show. */\n async pendingPairing(userCode: string): Promise<{\n label: string;\n platform: string;\n daemonVersion: string;\n capabilities: readonly { kind: string; model: string }[];\n expiresAt: number;\n } | null> {\n const pairing = await this.#store.getPairingByUserCode(\n normalizeUserCode(userCode),\n );\n if (pairing?.state !== \"pending\") return null;\n if (pairing.expiresAt <= this.#now()) return null;\n return {\n label: pairing.label,\n platform: pairing.platform,\n daemonVersion: pairing.daemonVersion,\n capabilities: pairing.capabilities.map((c) => ({\n kind: c.kind,\n model: c.model,\n })),\n expiresAt: pairing.expiresAt,\n };\n }\n\n /** The user's paired runners, for a settings page. */\n async runners(owner: string): Promise<RunnerRecord[]> {\n return this.#store.listRunners(owner);\n }\n\n /** Revoke a runner. It stops at its next heartbeat, mid-queue. */\n async revokeRunner(runnerId: string): Promise<void> {\n return this.#store.revokeRunner(runnerId, this.#now());\n }\n\n /** Run the expiry sweep. Idempotent; safe to call on a timer or a request. */\n async sweep(): Promise<JobRecord[]> {\n return this.#store.expireDue(this.#now());\n }\n}\n\n/**\n * Accept a pairing code however the user typed it — lowercase, spaces, no\n * dash. The code is displayed as `XXXX-XXXX`; refusing `xxxxxxxx` would fail\n * a user for a formatting detail they were never told mattered.\n */\nexport function normalizeUserCode(input: string): string {\n const bare = input.toUpperCase().replace(/[^A-Z0-9]/g, \"\");\n return bare.length === 8 ? `${bare.slice(0, 4)}-${bare.slice(4)}` : bare;\n}\n","import {\n backendDescriptor,\n matchAudience,\n type Capability,\n} from \"@byollm/protocol\";\nimport { generateJobId } from \"./ids.js\";\nimport type {\n EnqueueInput,\n JobRecord,\n PairingRecord,\n RunnerRecord,\n} from \"./records.js\";\nimport type {\n ApproveArgs,\n ByollmStore,\n ClaimArgs,\n CompleteArgs,\n CompleteResult,\n ReleaseArgs,\n RenewArgs,\n RenewResult,\n TouchArgs,\n} from \"./store.js\";\n\n/** Tunables an embedder may want to override in tests. */\nexport interface MemoryStoreOptions {\n /** Default TTL for a job once claimable. */\n readonly defaultTtlMs?: number;\n}\n\nconst DEFAULT_TTL_MS = 15 * 60_000;\n\n/**\n * The reference store: everything in one process, no persistence.\n *\n * This is not a toy — it is the implementation the conformance kit certifies\n * first, so its semantics *are* the specification's semantics for anything\n * the prose leaves implicit. A SQL adapter is correct when the same kit\n * passes against it.\n *\n * Concurrency: JavaScript's single-threaded turn is the atomicity primitive.\n * `claim` performs its read-decide-write with no `await` inside the critical\n * section, which is what makes {@link MUSTS.CLAIM_ATOMIC} hold here. A SQL\n * adapter gets the same property from `FOR UPDATE SKIP LOCKED`.\n */\nexport class MemoryStore implements ByollmStore {\n readonly #jobs = new Map<string, JobRecord>();\n readonly #runners = new Map<string, RunnerRecord>();\n readonly #pairings = new Map<string, PairingRecord>();\n /** Job ids the app has asked to cancel, not yet acknowledged by a runner. */\n readonly #cancelRequests = new Set<string>();\n readonly #defaultTtlMs: number;\n\n constructor(options: MemoryStoreOptions = {}) {\n this.#defaultTtlMs = options.defaultTtlMs ?? DEFAULT_TTL_MS;\n }\n\n // -- jobs ---------------------------------------------------------------\n\n create(input: EnqueueInput, now: number): Promise<JobRecord> {\n const id = input.id ?? generateJobId();\n const existing = this.#jobs.get(id);\n // Idempotent by caller-supplied id: re-enqueueing the same id is a no-op,\n // so an app's retry cannot duplicate work (the house \"one door\" rule).\n if (existing) return Promise.resolve(existing);\n\n const dependsOn = [...(input.dependsOn ?? [])];\n const blocked = dependsOn.some(\n (depId) => this.#jobs.get(depId)?.state !== \"ok\",\n );\n\n const job: JobRecord = {\n id,\n kind: input.kind,\n payload: input.payload,\n audience: input.audience ?? \"self\",\n owner: input.owner,\n audienceAllow: input.audienceAllow ? [...input.audienceAllow] : undefined,\n dependsOn,\n state: \"queued\",\n lease: null,\n createdAt: now,\n // The TTL clock starts here only if nothing blocks the job.\n claimableAt: blocked ? null : now,\n ttlMs: input.ttlMs ?? this.#defaultTtlMs,\n deadlineAt: input.deadlineAt ?? null,\n refusedBy: [],\n attempts: 0,\n outcome: null,\n provenance: null,\n updatedAt: now,\n };\n this.#jobs.set(id, job);\n return Promise.resolve(job);\n }\n\n get(jobId: string): Promise<JobRecord | null> {\n return Promise.resolve(this.#jobs.get(jobId) ?? null);\n }\n\n claim(args: ClaimArgs): Promise<JobRecord[]> {\n // Sweep first so an expired job is never handed out.\n this.#expireDueSync(args.now);\n\n const claimed: JobRecord[] = [];\n // Oldest-claimable first: a job that has waited longest goes next.\n const candidates = [...this.#jobs.values()].sort(\n (a, b) => (a.claimableAt ?? Infinity) - (b.claimableAt ?? Infinity),\n );\n\n for (const job of candidates) {\n if (claimed.length >= args.max) break;\n if (!this.#isClaimable(job, args)) continue;\n\n const updated: JobRecord = {\n ...job,\n state: \"claimed\",\n lease: {\n runnerId: args.runnerId,\n expiresAt: args.now + args.leaseMs,\n },\n attempts: job.attempts + 1,\n updatedAt: args.now,\n };\n this.#jobs.set(job.id, updated);\n claimed.push(updated);\n }\n return Promise.resolve(claimed);\n }\n\n /**\n * The claim predicate, shared by `claim` and the no-runner signal so the\n * two can never disagree about what \"a runner that could take this\" means.\n */\n #isClaimable(\n job: JobRecord,\n args: Pick<ClaimArgs, \"runnerId\" | \"runnerOwner\" | \"capabilities\" | \"now\">,\n ): boolean {\n if (job.state !== \"queued\") return false;\n if (job.claimableAt === null || job.claimableAt > args.now) return false;\n if (job.refusedBy.includes(args.runnerId)) return false;\n\n const capability = capabilityFor(args.capabilities, job.kind);\n if (!capability) return false;\n\n const match = matchAudience(\n {\n owner: job.owner,\n audience: job.audience,\n audienceAllow: job.audienceAllow,\n },\n {\n owner: args.runnerOwner,\n offerScope: capability.offerScope,\n // From the registry, not a local guess — the subscription self-lock\n // must mean the same thing on both sides of the wire.\n account: backendDescriptor(capability.backendId).account,\n // The server cannot see a remote daemon's local allowlist and must\n // not pretend to (protocol §4.2). It admits the job here; the daemon\n // is the enforcing side and releases with `refused` if its own list\n // says no.\n locallyAllows: () => true,\n },\n );\n return match.ok;\n }\n\n renewLeases(args: RenewArgs): Promise<RenewResult> {\n this.#expireDueSync(args.now);\n\n const renewed: { jobId: string; expiresAt: number }[] = [];\n const lost: string[] = [];\n\n for (const jobId of args.jobIds) {\n const job = this.#jobs.get(jobId);\n if (!job || job.lease?.runnerId !== args.runnerId) {\n // Either reclaimed by someone else or terminal — either way this\n // runner must stop working on it.\n lost.push(jobId);\n continue;\n }\n if (job.state !== \"claimed\" && job.state !== \"running\") {\n lost.push(jobId);\n continue;\n }\n const expiresAt = args.now + args.leaseMs;\n this.#jobs.set(jobId, {\n ...job,\n state: \"running\",\n lease: { runnerId: args.runnerId, expiresAt },\n updatedAt: args.now,\n });\n renewed.push({ jobId, expiresAt });\n }\n return Promise.resolve({ renewed, lost });\n }\n\n complete(args: CompleteArgs): Promise<CompleteResult> {\n const job = this.#jobs.get(args.jobId);\n if (!job) return Promise.resolve({ accepted: false, job: null });\n\n // First terminal outcome wins; a later submission is discarded, not\n // applied ({@link MUSTS.RESULT_IDEMPOTENT}).\n if (\n job.state === \"ok\" ||\n job.state === \"error\" ||\n job.state === \"canceled\"\n ) {\n return Promise.resolve({ accepted: false, job });\n }\n if (job.state === \"expired\") {\n return Promise.resolve({ accepted: false, job });\n }\n // A runner that lost its lease may not write a result\n // ({@link MUSTS.LEASE_HONORED}).\n if (job.lease?.runnerId !== args.runnerId) {\n return Promise.resolve({ accepted: false, job });\n }\n\n const state =\n args.outcome.outcome === \"ok\"\n ? \"ok\"\n : args.outcome.outcome === \"canceled\"\n ? \"canceled\"\n : \"error\";\n\n const updated: JobRecord = {\n ...job,\n state,\n lease: null,\n outcome: args.outcome,\n provenance: args.provenance,\n updatedAt: args.now,\n };\n this.#jobs.set(job.id, updated);\n this.#cancelRequests.delete(job.id);\n\n if (state === \"ok\") this.#unblockDependents(job.id, args.now);\n\n return Promise.resolve({ accepted: true, job: updated });\n }\n\n /**\n * Start the TTL clock on anything this job was blocking.\n *\n * Deliberately only on `ok`: a dependency that errored leaves its dependents\n * blocked forever rather than releasing them into a run whose input never\n * arrived. They expire at their absolute deadline if one was set, and the\n * app sees a chain that stopped where it broke.\n */\n #unblockDependents(completedId: string, now: number): void {\n for (const job of this.#jobs.values()) {\n if (job.claimableAt !== null) continue;\n if (!job.dependsOn.includes(completedId)) continue;\n const ready = job.dependsOn.every(\n (depId) => this.#jobs.get(depId)?.state === \"ok\",\n );\n if (ready) {\n this.#jobs.set(job.id, { ...job, claimableAt: now, updatedAt: now });\n }\n }\n }\n\n release(args: ReleaseArgs): Promise<string[]> {\n const released: string[] = [];\n for (const jobId of args.jobIds) {\n const job = this.#jobs.get(jobId);\n if (!job || job.lease?.runnerId !== args.runnerId) continue;\n\n this.#jobs.set(jobId, {\n ...job,\n state: \"queued\",\n lease: null,\n // Newly available again, so the TTL clock restarts here too.\n claimableAt: args.now,\n // A refusal is remembered, or the pair spins between claim and\n // release forever ({@link MUSTS.REFUSAL_NOT_REOFFERED}).\n refusedBy:\n args.reason === \"refused\"\n ? [...new Set([...job.refusedBy, args.runnerId])]\n : job.refusedBy,\n updatedAt: args.now,\n });\n released.push(jobId);\n }\n return Promise.resolve(released);\n }\n\n expireDue(now: number): Promise<JobRecord[]> {\n return Promise.resolve(this.#expireDueSync(now));\n }\n\n /**\n * Lease expiry and TTL expiry in one idempotent sweep.\n *\n * Order matters: a lease is reclaimed *before* the TTL is judged, so a job\n * whose runner died is offered again rather than being expired for having\n * sat in `claimed` too long.\n */\n #expireDueSync(now: number): JobRecord[] {\n const changed: JobRecord[] = [];\n\n for (const job of this.#jobs.values()) {\n if (\n (job.state === \"claimed\" || job.state === \"running\") &&\n job.lease !== null &&\n job.lease.expiresAt <= now\n ) {\n const requeued: JobRecord = {\n ...job,\n state: \"queued\",\n lease: null,\n // The TTL clock restarts: it measures how long a job has waited\n // *unclaimed*, and this job has just become available again. Without\n // this, a job whose runner died would expire for time it spent being\n // actively worked on — losing exactly the work `kill -9` recovery\n // exists to save. Total lifetime is bounded by `deadlineAt`, which\n // is absolute and unaffected by reclaim.\n claimableAt: now,\n updatedAt: now,\n };\n this.#jobs.set(job.id, requeued);\n changed.push(requeued);\n }\n }\n\n for (const job of this.#jobs.values()) {\n if (job.state !== \"queued\") continue;\n const pastDeadline = job.deadlineAt !== null && job.deadlineAt <= now;\n const pastTtl =\n job.claimableAt !== null && job.claimableAt + job.ttlMs <= now;\n if (!pastDeadline && !pastTtl) continue;\n\n const expired: JobRecord = {\n ...job,\n state: \"expired\",\n lease: null,\n updatedAt: now,\n };\n this.#jobs.set(job.id, expired);\n changed.push(expired);\n }\n return changed;\n }\n\n cancel(jobId: string, now: number): Promise<JobRecord | null> {\n const job = this.#jobs.get(jobId);\n if (!job) return Promise.resolve(null);\n if (job.state === \"queued\") {\n const canceled: JobRecord = {\n ...job,\n state: \"canceled\",\n lease: null,\n updatedAt: now,\n };\n this.#jobs.set(jobId, canceled);\n return Promise.resolve(canceled);\n }\n if (job.state === \"claimed\" || job.state === \"running\") {\n // Held by a runner: the cancel travels on the next heartbeat and the\n // runner reports `canceled` itself, so the job's own state waits.\n this.#cancelRequests.add(jobId);\n return Promise.resolve(job);\n }\n return Promise.resolve(job);\n }\n\n listClaimedBy(runnerId: string): Promise<JobRecord[]> {\n return Promise.resolve(\n [...this.#jobs.values()].filter(\n (job) => job.lease?.runnerId === runnerId,\n ),\n );\n }\n\n listCancelRequests(runnerId: string): Promise<string[]> {\n return Promise.resolve(\n [...this.#cancelRequests].filter(\n (jobId) => this.#jobs.get(jobId)?.lease?.runnerId === runnerId,\n ),\n );\n }\n\n // -- pairing and runners -------------------------------------------------\n\n createPairing(record: PairingRecord): Promise<void> {\n this.#pairings.set(record.deviceCodeHash, record);\n return Promise.resolve();\n }\n\n getPairingByDeviceCodeHash(hash: string): Promise<PairingRecord | null> {\n return Promise.resolve(this.#pairings.get(hash) ?? null);\n }\n\n getPairingByUserCode(userCode: string): Promise<PairingRecord | null> {\n for (const pairing of this.#pairings.values()) {\n if (pairing.userCode === userCode) return Promise.resolve(pairing);\n }\n return Promise.resolve(null);\n }\n\n approvePairing(args: ApproveArgs): Promise<RunnerRecord> {\n const pairing = [...this.#pairings.values()].find(\n (p) => p.userCode === args.userCode,\n );\n if (!pairing) throw new Error(`unknown pairing code: ${args.userCode}`);\n if (pairing.expiresAt <= args.now) {\n throw new Error(\"pairing code has expired\");\n }\n if (pairing.state !== \"pending\") {\n throw new Error(`pairing is already ${pairing.state}`);\n }\n\n const runner: RunnerRecord = {\n id: args.runnerId,\n owner: args.owner,\n tokenHash: args.tokenHash,\n label: pairing.label,\n platform: pairing.platform,\n daemonVersion: pairing.daemonVersion,\n capabilities: pairing.capabilities,\n paused: false,\n revokedAt: null,\n lastHeartbeatAt: args.now,\n createdAt: args.now,\n };\n this.#runners.set(runner.id, runner);\n this.#pairings.set(pairing.deviceCodeHash, {\n ...pairing,\n state: \"approved\",\n owner: args.owner,\n runnerId: runner.id,\n runnerTokenOnce: args.runnerToken,\n });\n return Promise.resolve(runner);\n }\n\n denyPairing(userCode: string, _now: number): Promise<void> {\n const pairing = [...this.#pairings.values()].find(\n (p) => p.userCode === userCode,\n );\n if (pairing) {\n this.#pairings.set(pairing.deviceCodeHash, {\n ...pairing,\n state: \"denied\",\n });\n }\n return Promise.resolve();\n }\n\n consumePairingToken(deviceCodeHash: string): Promise<void> {\n const pairing = this.#pairings.get(deviceCodeHash);\n if (pairing) {\n this.#pairings.set(deviceCodeHash, {\n ...pairing,\n runnerTokenOnce: null,\n });\n }\n return Promise.resolve();\n }\n\n getRunnerByTokenHash(hash: string): Promise<RunnerRecord | null> {\n for (const runner of this.#runners.values()) {\n if (runner.tokenHash === hash) return Promise.resolve(runner);\n }\n return Promise.resolve(null);\n }\n\n getRunner(runnerId: string): Promise<RunnerRecord | null> {\n return Promise.resolve(this.#runners.get(runnerId) ?? null);\n }\n\n touchRunner(args: TouchArgs): Promise<RunnerRecord | null> {\n const runner = this.#runners.get(args.runnerId);\n if (!runner) return Promise.resolve(null);\n const updated: RunnerRecord = {\n ...runner,\n capabilities: args.capabilities,\n daemonVersion: args.daemonVersion,\n paused: args.paused,\n lastHeartbeatAt: args.now,\n };\n this.#runners.set(runner.id, updated);\n return Promise.resolve(updated);\n }\n\n revokeRunner(runnerId: string, now: number): Promise<void> {\n const runner = this.#runners.get(runnerId);\n // Revocation is one-way: an already-revoked runner keeps its first\n // revocation time rather than being re-stamped.\n if (runner?.revokedAt === null) {\n this.#runners.set(runnerId, { ...runner, revokedAt: now });\n }\n return Promise.resolve();\n }\n\n listRunners(owner?: string): Promise<RunnerRecord[]> {\n const all = [...this.#runners.values()];\n return Promise.resolve(\n owner === undefined ? all : all.filter((r) => r.owner === owner),\n );\n }\n\n // -- test/demo helpers ---------------------------------------------------\n\n /** All jobs, for demos and assertions. Not part of the store interface. */\n allJobs(): JobRecord[] {\n return [...this.#jobs.values()];\n }\n}\n\n/** The capability that would serve a kind, if any. */\nexport function capabilityFor(\n capabilities: readonly Capability[],\n kind: string,\n): Capability | undefined {\n return capabilities.find((c) => c.kind === kind);\n}\n"],"mappings":"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","names":["backendDescriptor","matchAudience","matchAudience","backendDescriptor"]}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { H as HandlerConfig } from './handlers-D7lWfwno.js';
|
|
2
|
+
import '@byollm/protocol';
|
|
3
|
+
import './store-D23N6iiP.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `@byollm/server/next` — the one-file Next.js mount.
|
|
7
|
+
*
|
|
8
|
+
* Drop this in `app/api/byollm/[...route]/route.ts`:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { createHandler } from "@byollm/server/next";
|
|
12
|
+
* import { store } from "@/lib/byollm";
|
|
13
|
+
*
|
|
14
|
+
* export const { POST } = createHandler({
|
|
15
|
+
* store,
|
|
16
|
+
* verificationUrl: "https://your-app.com/settings/runners",
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* That is the whole protocol surface. The app-facing half — enqueue, approve
|
|
21
|
+
* a pairing, read a result — is {@link ByollmApp} from `@byollm/server`.
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
declare function createHandler(config: HandlerConfig): {
|
|
26
|
+
POST: (request: Request) => Promise<Response>;
|
|
27
|
+
/** Present so a stray GET gets a clear 405 rather than a framework 404. */
|
|
28
|
+
GET: (request: Request) => Promise<Response>;
|
|
29
|
+
/** Route handlers must not be cached — every call mutates lease state. */
|
|
30
|
+
dynamic: "force-dynamic";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export { HandlerConfig, createHandler };
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFetchHandler
|
|
3
|
+
} from "./chunk-HL6EYHQ7.js";
|
|
4
|
+
|
|
5
|
+
// src/next.ts
|
|
6
|
+
function createHandler(config) {
|
|
7
|
+
const handler = createFetchHandler(config);
|
|
8
|
+
return {
|
|
9
|
+
POST: handler,
|
|
10
|
+
GET: handler,
|
|
11
|
+
dynamic: "force-dynamic"
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
createHandler
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=next.js.map
|
package/dist/next.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/next.ts"],"sourcesContent":["import { createFetchHandler } from \"./http.js\";\nimport type { HandlerConfig } from \"./handlers.js\";\n\n/**\n * `@byollm/server/next` — the one-file Next.js mount.\n *\n * Drop this in `app/api/byollm/[...route]/route.ts`:\n *\n * ```ts\n * import { createHandler } from \"@byollm/server/next\";\n * import { store } from \"@/lib/byollm\";\n *\n * export const { POST } = createHandler({\n * store,\n * verificationUrl: \"https://your-app.com/settings/runners\",\n * });\n * ```\n *\n * That is the whole protocol surface. The app-facing half — enqueue, approve\n * a pairing, read a result — is {@link ByollmApp} from `@byollm/server`.\n *\n * @packageDocumentation\n */\nexport function createHandler(config: HandlerConfig): {\n POST: (request: Request) => Promise<Response>;\n /** Present so a stray GET gets a clear 405 rather than a framework 404. */\n GET: (request: Request) => Promise<Response>;\n /** Route handlers must not be cached — every call mutates lease state. */\n dynamic: \"force-dynamic\";\n} {\n const handler = createFetchHandler(config);\n return {\n POST: handler,\n GET: handler,\n dynamic: \"force-dynamic\",\n };\n}\n\nexport type { HandlerConfig };\n"],"mappings":";;;;;AAuBO,SAAS,cAAc,QAM5B;AACA,QAAM,UAAU,mBAAmB,MAAM;AACzC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,KAAK;AAAA,IACL,SAAS;AAAA,EACX;AACF;","names":[]}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { JobKind, JobPayload, Audience, JobState, Lease, JobOutcome, ResultProvenance, Capability } from '@byollm/protocol';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A job as the server stores it.
|
|
5
|
+
*
|
|
6
|
+
* Adapters map this shape onto their own storage; the field meanings are
|
|
7
|
+
* normative because the conformance kit asserts behaviour that depends on
|
|
8
|
+
* them (TTL clock start, dependency gating, refusal tracking).
|
|
9
|
+
*/
|
|
10
|
+
interface JobRecord {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly kind: JobKind;
|
|
13
|
+
readonly payload: JobPayload;
|
|
14
|
+
readonly audience: Audience;
|
|
15
|
+
/** The app's id for the user who enqueued it. */
|
|
16
|
+
readonly owner: string;
|
|
17
|
+
/** Server-side restriction on which runner owners may take a `named` job. */
|
|
18
|
+
readonly audienceAllow: readonly string[] | undefined;
|
|
19
|
+
/** Job ids that must all be `ok` before this becomes claimable. */
|
|
20
|
+
readonly dependsOn: readonly string[];
|
|
21
|
+
readonly state: JobState;
|
|
22
|
+
readonly lease: Lease | null;
|
|
23
|
+
readonly createdAt: number;
|
|
24
|
+
/**
|
|
25
|
+
* When the job became claimable — enqueue time for a job with no
|
|
26
|
+
* dependencies, or the moment its last dependency reached `ok`.
|
|
27
|
+
*
|
|
28
|
+
* **The TTL clock starts here, not at `createdAt`.** Starting it at enqueue
|
|
29
|
+
* would expire a dependent job for the crime of waiting on a slow
|
|
30
|
+
* dependency (byollm_001 Rev 1 §D, TTL clock resolved in build review).
|
|
31
|
+
* `null` means still blocked.
|
|
32
|
+
*/
|
|
33
|
+
readonly claimableAt: number | null;
|
|
34
|
+
/** How long an unclaimed job may wait once claimable. */
|
|
35
|
+
readonly ttlMs: number;
|
|
36
|
+
/** Optional absolute deadline, independent of the TTL. */
|
|
37
|
+
readonly deadlineAt: number | null;
|
|
38
|
+
/**
|
|
39
|
+
* Runners that released this job with reason `refused` — their local
|
|
40
|
+
* allowlist declined it. Never offered to them again
|
|
41
|
+
* ({@link MUSTS.REFUSAL_NOT_REOFFERED}).
|
|
42
|
+
*/
|
|
43
|
+
readonly refusedBy: readonly string[];
|
|
44
|
+
/** How many times this job has been claimed, including lease-expiry retries. */
|
|
45
|
+
readonly attempts: number;
|
|
46
|
+
readonly outcome: JobOutcome | null;
|
|
47
|
+
readonly provenance: ResultProvenance | null;
|
|
48
|
+
readonly updatedAt: number;
|
|
49
|
+
}
|
|
50
|
+
/** A paired daemon as the server stores it. */
|
|
51
|
+
interface RunnerRecord {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
/** The app's id for the user this runner is bound to — exactly one. */
|
|
54
|
+
readonly owner: string;
|
|
55
|
+
/** SHA-256 of the bearer token. The token itself is never stored. */
|
|
56
|
+
readonly tokenHash: string;
|
|
57
|
+
readonly label: string;
|
|
58
|
+
readonly platform: "darwin" | "linux" | "win32";
|
|
59
|
+
readonly daemonVersion: string;
|
|
60
|
+
readonly capabilities: readonly Capability[];
|
|
61
|
+
readonly paused: boolean;
|
|
62
|
+
/** Set once; a revoked runner never un-revokes. */
|
|
63
|
+
readonly revokedAt: number | null;
|
|
64
|
+
readonly lastHeartbeatAt: number;
|
|
65
|
+
readonly createdAt: number;
|
|
66
|
+
}
|
|
67
|
+
/** An in-flight device-code pairing. */
|
|
68
|
+
interface PairingRecord {
|
|
69
|
+
/** SHA-256 of the device code. The code itself is never stored. */
|
|
70
|
+
readonly deviceCodeHash: string;
|
|
71
|
+
/** The short code the user reads. Unique among live pairings. */
|
|
72
|
+
readonly userCode: string;
|
|
73
|
+
readonly state: "pending" | "approved" | "denied";
|
|
74
|
+
/** Set when approved — learned from the approving user's own session. */
|
|
75
|
+
readonly owner: string | null;
|
|
76
|
+
readonly runnerId: string | null;
|
|
77
|
+
/**
|
|
78
|
+
* The bearer token, held until the daemon's next poll collects it, then
|
|
79
|
+
* cleared. Delivered exactly once.
|
|
80
|
+
*/
|
|
81
|
+
readonly runnerTokenOnce: string | null;
|
|
82
|
+
readonly label: string;
|
|
83
|
+
readonly platform: "darwin" | "linux" | "win32";
|
|
84
|
+
readonly daemonVersion: string;
|
|
85
|
+
readonly capabilities: readonly Capability[];
|
|
86
|
+
readonly expiresAt: number;
|
|
87
|
+
readonly createdAt: number;
|
|
88
|
+
}
|
|
89
|
+
/** What the app supplies to enqueue a job. */
|
|
90
|
+
interface EnqueueInput {
|
|
91
|
+
readonly kind: JobKind;
|
|
92
|
+
readonly payload: JobPayload;
|
|
93
|
+
readonly owner: string;
|
|
94
|
+
/** Defaults to `self` — the safe direction. */
|
|
95
|
+
readonly audience?: Audience;
|
|
96
|
+
readonly audienceAllow?: readonly string[];
|
|
97
|
+
readonly dependsOn?: readonly string[];
|
|
98
|
+
/** Defaults to the server config's `defaultTtlMs`. */
|
|
99
|
+
readonly ttlMs?: number;
|
|
100
|
+
readonly deadlineAt?: number;
|
|
101
|
+
/** Caller-supplied id, for idempotent enqueue. */
|
|
102
|
+
readonly id?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The adapter seam.
|
|
107
|
+
*
|
|
108
|
+
* Everything in `@byollm/server` above this interface is storage-agnostic;
|
|
109
|
+
* everything below it is one adapter. `MemoryJobStore` is the reference
|
|
110
|
+
* implementation and the thing the conformance kit certifies first — an
|
|
111
|
+
* adapter is correct when the same kit passes against it.
|
|
112
|
+
*/
|
|
113
|
+
interface JobStore {
|
|
114
|
+
/** Create a job. Idempotent when `input.id` is supplied and already exists. */
|
|
115
|
+
create(input: EnqueueInput, now: number): Promise<JobRecord>;
|
|
116
|
+
get(jobId: string): Promise<JobRecord | null>;
|
|
117
|
+
/**
|
|
118
|
+
* Atomically claim up to `max` jobs for a runner
|
|
119
|
+
* ({@link MUSTS.CLAIM_ATOMIC}).
|
|
120
|
+
*
|
|
121
|
+
* An implementation MUST apply, inside the same atomic step:
|
|
122
|
+
* - state is `queued`;
|
|
123
|
+
* - `claimableAt` is non-null and `<= now` (dependencies satisfied,
|
|
124
|
+
* {@link MUSTS.DEPENDS_ON_GATING});
|
|
125
|
+
* - the job's kind appears in `capabilities`
|
|
126
|
+
* ({@link MUSTS.CLAIM_REQUIRES_CAPABILITY});
|
|
127
|
+
* - the audience rules admit this runner
|
|
128
|
+
* ({@link MUSTS.AUDIENCE_BOTH_SIDES});
|
|
129
|
+
* - `runnerId` is not in the job's `refusedBy`
|
|
130
|
+
* ({@link MUSTS.REFUSAL_NOT_REOFFERED}).
|
|
131
|
+
*
|
|
132
|
+
* SQL-backed adapters SHOULD use `FOR UPDATE SKIP LOCKED`.
|
|
133
|
+
*/
|
|
134
|
+
claim(args: ClaimArgs): Promise<JobRecord[]>;
|
|
135
|
+
/**
|
|
136
|
+
* Renew leases for the jobs a runner believes it holds, and report which it
|
|
137
|
+
* has lost. A job whose lease expired un-renewed returns to `queued`
|
|
138
|
+
* ({@link MUSTS.LEASE_RECLAIMABLE}).
|
|
139
|
+
*/
|
|
140
|
+
renewLeases(args: RenewArgs): Promise<RenewResult>;
|
|
141
|
+
/**
|
|
142
|
+
* Record a terminal outcome. Idempotent by job id: the first terminal
|
|
143
|
+
* outcome wins ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
144
|
+
*
|
|
145
|
+
* Recording `ok` MUST also unblock dependents whose remaining dependencies
|
|
146
|
+
* are all `ok`, setting their `claimableAt` — which is when their TTL clock
|
|
147
|
+
* starts ({@link MUSTS.TTL_EXPIRY}).
|
|
148
|
+
*/
|
|
149
|
+
complete(args: CompleteArgs): Promise<CompleteResult>;
|
|
150
|
+
/**
|
|
151
|
+
* Return jobs to `queued`. When `reason` is `refused`, the runner MUST be
|
|
152
|
+
* added to each job's `refusedBy`.
|
|
153
|
+
*/
|
|
154
|
+
release(args: ReleaseArgs): Promise<string[]>;
|
|
155
|
+
/**
|
|
156
|
+
* Move every claimable-but-unclaimed job past its TTL, and every job past
|
|
157
|
+
* its absolute deadline, to `expired`. Returns what changed.
|
|
158
|
+
*
|
|
159
|
+
* Called opportunistically by the handlers; an adapter MAY also run it on a
|
|
160
|
+
* schedule. It MUST be idempotent — firing twice is always safe.
|
|
161
|
+
*/
|
|
162
|
+
expireDue(now: number): Promise<JobRecord[]>;
|
|
163
|
+
/** Cancel a job by app request. Returns the job, or null if unknown. */
|
|
164
|
+
cancel(jobId: string, now: number): Promise<JobRecord | null>;
|
|
165
|
+
/** Jobs a runner currently holds — used to build the heartbeat cancel list. */
|
|
166
|
+
listClaimedBy(runnerId: string): Promise<JobRecord[]>;
|
|
167
|
+
/** Jobs awaiting cancellation that a given runner holds. */
|
|
168
|
+
listCancelRequests(runnerId: string): Promise<string[]>;
|
|
169
|
+
}
|
|
170
|
+
interface ClaimArgs {
|
|
171
|
+
readonly runnerId: string;
|
|
172
|
+
/** The runner's owner, for audience matching. */
|
|
173
|
+
readonly runnerOwner: string;
|
|
174
|
+
readonly capabilities: readonly Capability[];
|
|
175
|
+
readonly max: number;
|
|
176
|
+
readonly leaseMs: number;
|
|
177
|
+
readonly now: number;
|
|
178
|
+
}
|
|
179
|
+
interface RenewArgs {
|
|
180
|
+
readonly runnerId: string;
|
|
181
|
+
readonly jobIds: readonly string[];
|
|
182
|
+
readonly leaseMs: number;
|
|
183
|
+
readonly now: number;
|
|
184
|
+
}
|
|
185
|
+
interface RenewResult {
|
|
186
|
+
readonly renewed: readonly {
|
|
187
|
+
jobId: string;
|
|
188
|
+
expiresAt: number;
|
|
189
|
+
}[];
|
|
190
|
+
/** Jobs the runner claimed to hold but no longer does. */
|
|
191
|
+
readonly lost: readonly string[];
|
|
192
|
+
}
|
|
193
|
+
interface CompleteArgs {
|
|
194
|
+
readonly jobId: string;
|
|
195
|
+
readonly runnerId: string;
|
|
196
|
+
readonly outcome: JobOutcome;
|
|
197
|
+
readonly provenance: ResultProvenance;
|
|
198
|
+
readonly now: number;
|
|
199
|
+
}
|
|
200
|
+
interface CompleteResult {
|
|
201
|
+
/** False when this submission lost an idempotency race or the lease was gone. */
|
|
202
|
+
readonly accepted: boolean;
|
|
203
|
+
readonly job: JobRecord | null;
|
|
204
|
+
}
|
|
205
|
+
interface ReleaseArgs {
|
|
206
|
+
readonly runnerId: string;
|
|
207
|
+
readonly jobIds: readonly string[];
|
|
208
|
+
readonly reason: "shutdown" | "pause" | "revoked" | "backend-down" | "refused";
|
|
209
|
+
readonly now: number;
|
|
210
|
+
}
|
|
211
|
+
/** Runner registry and pairing state. */
|
|
212
|
+
interface RunnerStore {
|
|
213
|
+
/** Begin a device-code pairing. */
|
|
214
|
+
createPairing(record: PairingRecord): Promise<void>;
|
|
215
|
+
getPairingByDeviceCodeHash(hash: string): Promise<PairingRecord | null>;
|
|
216
|
+
getPairingByUserCode(userCode: string): Promise<PairingRecord | null>;
|
|
217
|
+
/**
|
|
218
|
+
* Approve a pairing on behalf of an authenticated user, creating the
|
|
219
|
+
* runner. The `owner` MUST come from the approving user's own session — a
|
|
220
|
+
* daemon can never assert who it is ({@link MUSTS.PAIR_ONE_USER}).
|
|
221
|
+
*/
|
|
222
|
+
approvePairing(args: ApproveArgs): Promise<RunnerRecord>;
|
|
223
|
+
denyPairing(userCode: string, now: number): Promise<void>;
|
|
224
|
+
/** Clear the one-shot token after the daemon collects it. */
|
|
225
|
+
consumePairingToken(deviceCodeHash: string): Promise<void>;
|
|
226
|
+
getRunnerByTokenHash(hash: string): Promise<RunnerRecord | null>;
|
|
227
|
+
getRunner(runnerId: string): Promise<RunnerRecord | null>;
|
|
228
|
+
/** Record a heartbeat: capabilities, version, pause state, liveness. */
|
|
229
|
+
touchRunner(args: TouchArgs): Promise<RunnerRecord | null>;
|
|
230
|
+
/** Revoke a runner. Once revoked, never un-revoked. */
|
|
231
|
+
revokeRunner(runnerId: string, now: number): Promise<void>;
|
|
232
|
+
/** Live runners for an owner — used by the no-runner signal. */
|
|
233
|
+
listRunners(owner?: string): Promise<RunnerRecord[]>;
|
|
234
|
+
}
|
|
235
|
+
interface ApproveArgs {
|
|
236
|
+
readonly userCode: string;
|
|
237
|
+
/** From the approving user's session, never from the daemon. */
|
|
238
|
+
readonly owner: string;
|
|
239
|
+
readonly runnerId: string;
|
|
240
|
+
readonly runnerToken: string;
|
|
241
|
+
readonly tokenHash: string;
|
|
242
|
+
readonly now: number;
|
|
243
|
+
}
|
|
244
|
+
interface TouchArgs {
|
|
245
|
+
readonly runnerId: string;
|
|
246
|
+
readonly capabilities: readonly Capability[];
|
|
247
|
+
readonly daemonVersion: string;
|
|
248
|
+
readonly paused: boolean;
|
|
249
|
+
readonly now: number;
|
|
250
|
+
}
|
|
251
|
+
/** A store providing both halves. Most adapters implement one object. */
|
|
252
|
+
interface ByollmStore extends JobStore, RunnerStore {
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export type { ApproveArgs as A, ByollmStore as B, ClaimArgs as C, EnqueueInput as E, JobRecord as J, PairingRecord as P, RunnerRecord as R, TouchArgs as T, RenewArgs as a, RenewResult as b, CompleteArgs as c, CompleteResult as d, ReleaseArgs as e, JobStore as f, RunnerStore as g };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import { B as ByollmStore } from '../store-D23N6iiP.js';
|
|
3
|
+
import { P as PollingDeliveryDeps, R as ResultDelivery } from '../delivery-36nIe-b3.js';
|
|
4
|
+
import '@byollm/protocol';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Realtime delivery: the app learns a job finished when Postgres says so.
|
|
8
|
+
*
|
|
9
|
+
* byollm_003 Rev 1 requires the server→app path be an explicit channel rather
|
|
10
|
+
* than an implied in-request `await`. Polling is the portable default;
|
|
11
|
+
* this is the one worth having when the app is already on Supabase, because
|
|
12
|
+
* a result arrives in milliseconds instead of on the next poll tick.
|
|
13
|
+
*
|
|
14
|
+
* The no-runner watch still polls, deliberately: runner liveness is a
|
|
15
|
+
* *derived* signal (nobody with matching capability has heartbeated lately),
|
|
16
|
+
* and there is no row change to subscribe to for "something stopped
|
|
17
|
+
* happening".
|
|
18
|
+
*/
|
|
19
|
+
declare function supabaseRealtimeDelivery(client: SupabaseClient): (deps: PollingDeliveryDeps) => ResultDelivery;
|
|
20
|
+
|
|
21
|
+
interface SupabaseStoreOptions {
|
|
22
|
+
/** A client built with the **service role** key. */
|
|
23
|
+
readonly client: SupabaseClient;
|
|
24
|
+
/** Default TTL for a job once claimable. */
|
|
25
|
+
readonly defaultTtlMs?: number;
|
|
26
|
+
}
|
|
27
|
+
/** Build the Supabase-backed store. */
|
|
28
|
+
declare function supabaseStore(options: SupabaseStoreOptions): ByollmStore;
|
|
29
|
+
|
|
30
|
+
export { type SupabaseStoreOptions, supabaseRealtimeDelivery, supabaseStore };
|