or3-provider-sqlite 0.0.7 → 0.0.9

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/README.md CHANGED
@@ -125,9 +125,11 @@ On server startup, the Nitro plugin:
125
125
  6. Registers `WebhookStore` and the admin stores (workspace access, workspace
126
126
  settings, user search) — all runtimes except D1
127
127
  7. Registers `RateLimitProvider` with ID `sqlite` and the sync admin adapter
128
+ 8. Registers a durable `BackgroundJobProvider` with ID `sqlite`, so active chat
129
+ and workflow jobs remain addressable across Nitro HMR/module reloads
128
130
 
129
131
  Registration is skipped when `auth.enabled` is `false`, or when neither SQLite
130
- sync nor SQLite Connect is selected (local-only mode).
132
+ sync, SQLite Connect, nor SQLite background jobs are selected (local-only mode).
131
133
 
132
134
  ### Schema
133
135
 
@@ -138,6 +140,8 @@ Ordered migrations create and evolve all tables:
138
140
  - **003–005**: workspace-scoped sync keys, invitations, and admin stores
139
141
  - **006_sync_snapshots**: winning operation IDs plus immutable snapshot headers/items
140
142
  - **009_or3_connect**: single-use device authorizations and connected computers
143
+ - **017_background_jobs**: durable job status, workflow snapshots, cancellation,
144
+ inactivity timestamps, and worker leases
141
145
 
142
146
  Additional migrations (007–008, 010–016) evolve device-cursor ownership,
143
147
  upload intents, and Connect credential/lifecycle hardening and rate limits.
@@ -146,7 +150,7 @@ All tables use snake_case aligned with the sync wire format.
146
150
 
147
151
  ### Sync semantics
148
152
 
149
- - **Push**: validates ops → checks `op_id` idempotency → allocates contiguous `server_version` block → writes change_log → applies LWW to materialized tables → upserts tombstones for deletes
153
+ - **Push**: validates ops (including a 256 KB serialized payload ceiling per operation) → checks `op_id` idempotency → allocates contiguous `server_version` block → writes change_log → applies LWW to materialized tables → upserts tombstones for deletes
150
154
  - **Pull**: returns ordered changes for `server_version > cursor` with limit/pagination and optional table filtering
151
155
  - **Snapshot**: captures canonical live rows and current tombstones at one `highWatermark`, then serves immutable, keyset-paginated pages ordered by `(tableName, pk, kind)`
152
156
  - **Cursor**: forward-only per-device cursor tracking
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "or3-provider-sqlite",
3
3
  "configKey": "or3-provider-sqlite",
4
- "version": "0.0.7",
4
+ "version": "0.0.9",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -0,0 +1,20 @@
1
+ import type { BackgroundJob, BackgroundJobExecution, BackgroundJobProvider, CreateJobParams, JobUpdate } from '~~/server/utils/background-jobs/types';
2
+ export declare class SqliteBackgroundJobProvider implements BackgroundJobProvider {
3
+ readonly name = "sqlite";
4
+ createJob(params: CreateJobParams): Promise<string>;
5
+ getJob(jobId: string, userId: string): Promise<BackgroundJob | null>;
6
+ updateJob(jobId: string, update: JobUpdate): Promise<void>;
7
+ completeJob(jobId: string, finalContent: string, leaseOwner?: string): Promise<void>;
8
+ failJob(jobId: string, error: string, leaseOwner?: string): Promise<void>;
9
+ private finish;
10
+ abortJob(jobId: string, userId: string): Promise<boolean>;
11
+ getAbortController(): undefined;
12
+ checkJobAborted(jobId: string): Promise<boolean>;
13
+ claimJob(jobId: string, leaseOwner: string, now: number, leaseExpiresAt: number): Promise<BackgroundJob | null>;
14
+ claimNextJob(leaseOwner: string, now: number, leaseExpiresAt: number): Promise<BackgroundJob | null>;
15
+ renewJobLease(jobId: string, leaseOwner: string, _now: number, leaseExpiresAt: number): Promise<boolean>;
16
+ updateJobExecution(jobId: string, execution: BackgroundJobExecution, leaseOwner: string): Promise<boolean>;
17
+ cleanupExpired(): Promise<number>;
18
+ getActiveJobCount(): Promise<number>;
19
+ }
20
+ export declare const sqliteBackgroundJobProvider: SqliteBackgroundJobProvider;
@@ -0,0 +1,299 @@
1
+ import { getJobConfig } from "~~/server/utils/background-jobs/store";
2
+ import { randomUUID } from "node:crypto";
3
+ import { d1All, d1Run } from "../db/d1.js";
4
+ import { getRawDb, isD1Driver } from "../db/kysely.js";
5
+ function parseJson(value) {
6
+ if (!value) return void 0;
7
+ return JSON.parse(value);
8
+ }
9
+ function json(value) {
10
+ return value === void 0 ? null : JSON.stringify(value);
11
+ }
12
+ function toJob(row) {
13
+ return {
14
+ id: row.id,
15
+ userId: row.user_id,
16
+ threadId: row.thread_id,
17
+ messageId: row.message_id,
18
+ model: row.model,
19
+ kind: row.kind ?? void 0,
20
+ status: row.status,
21
+ content: row.content,
22
+ chunksReceived: row.chunks_received,
23
+ startedAt: row.started_at,
24
+ lastActivityAt: row.last_activity_at,
25
+ completedAt: row.completed_at ?? void 0,
26
+ error: row.error ?? void 0,
27
+ tool_calls: parseJson(row.tool_calls_json),
28
+ workflow_state: parseJson(
29
+ row.workflow_state_json
30
+ ),
31
+ execution: parseJson(row.execution_json),
32
+ leaseOwner: row.lease_owner ?? void 0,
33
+ leaseExpiresAt: row.lease_expires_at ?? void 0,
34
+ attempts: row.attempts
35
+ };
36
+ }
37
+ async function all(statement, ...parameters) {
38
+ if (isD1Driver()) return d1All(statement, ...parameters);
39
+ return getRawDb().prepare(statement).all(...parameters);
40
+ }
41
+ async function one(statement, ...parameters) {
42
+ if (isD1Driver()) return (await d1All(statement, ...parameters))[0];
43
+ return getRawDb().prepare(statement).get(...parameters);
44
+ }
45
+ async function run(statement, ...parameters) {
46
+ if (isD1Driver()) {
47
+ const result = await d1Run(statement, ...parameters);
48
+ return result.meta?.changes ?? 0;
49
+ }
50
+ return getRawDb().prepare(statement).run(...parameters).changes;
51
+ }
52
+ function leaseLost() {
53
+ const error = new Error("Background job lease was superseded");
54
+ error.name = "BackgroundJobLeaseLostError";
55
+ return error;
56
+ }
57
+ function insertParameters(id, params, now) {
58
+ return [
59
+ id,
60
+ params.userId,
61
+ params.threadId,
62
+ params.messageId,
63
+ params.model,
64
+ params.kind ?? null,
65
+ now,
66
+ now,
67
+ json(params.tool_calls),
68
+ json(params.workflow_state),
69
+ json(params.execution),
70
+ params.idempotencyKey ?? null
71
+ ];
72
+ }
73
+ const INSERT_JOB = `INSERT INTO background_jobs (
74
+ id, user_id, thread_id, message_id, model, kind, status, content,
75
+ chunks_received, started_at, last_activity_at, tool_calls_json,
76
+ workflow_state_json, execution_json, idempotency_key, attempts
77
+ ) VALUES (?, ?, ?, ?, ?, ?, 'streaming', '', 0, ?, ?, ?, ?, ?, ?, 0)`;
78
+ const INSERT_JOB_IF_CAPACITY = `INSERT INTO background_jobs (
79
+ id, user_id, thread_id, message_id, model, kind, status, content,
80
+ chunks_received, started_at, last_activity_at, tool_calls_json,
81
+ workflow_state_json, execution_json, idempotency_key, attempts
82
+ ) SELECT ?, ?, ?, ?, ?, ?, 'streaming', '', 0, ?, ?, ?, ?, ?, ?, 0
83
+ WHERE (SELECT COUNT(*) FROM background_jobs WHERE status = 'streaming') < ?
84
+ AND (SELECT COUNT(*) FROM background_jobs WHERE status = 'streaming' AND user_id = ?) < ?
85
+ ON CONFLICT(idempotency_key) DO NOTHING
86
+ RETURNING id`;
87
+ export class SqliteBackgroundJobProvider {
88
+ name = "sqlite";
89
+ async createJob(params) {
90
+ const now = Date.now();
91
+ const id = randomUUID();
92
+ const config = getJobConfig();
93
+ if (isD1Driver()) {
94
+ if (params.idempotencyKey) {
95
+ const existing = await one(
96
+ "SELECT id FROM background_jobs WHERE idempotency_key = ?",
97
+ params.idempotencyKey
98
+ );
99
+ if (existing) return existing.id;
100
+ }
101
+ const inserted = await all(
102
+ INSERT_JOB_IF_CAPACITY,
103
+ ...insertParameters(id, params, now),
104
+ config.maxConcurrentJobs,
105
+ params.userId,
106
+ config.maxConcurrentJobsPerUser
107
+ );
108
+ if (inserted[0]) return inserted[0].id;
109
+ if (params.idempotencyKey) {
110
+ const existing = await one(
111
+ "SELECT id FROM background_jobs WHERE idempotency_key = ?",
112
+ params.idempotencyKey
113
+ );
114
+ if (existing) return existing.id;
115
+ }
116
+ throw new Error("Maximum concurrent background jobs reached");
117
+ }
118
+ const raw = getRawDb();
119
+ return raw.transaction(() => {
120
+ if (params.idempotencyKey) {
121
+ const existing = raw.prepare(
122
+ "SELECT id FROM background_jobs WHERE idempotency_key = ?"
123
+ ).get(params.idempotencyKey);
124
+ if (existing) return existing.id;
125
+ }
126
+ const active = raw.prepare(
127
+ `SELECT COUNT(*) AS total,
128
+ SUM(CASE WHEN user_id = ? THEN 1 ELSE 0 END) AS user_total
129
+ FROM background_jobs WHERE status = 'streaming'`
130
+ ).get(params.userId);
131
+ if (active.total >= config.maxConcurrentJobs || (active.user_total ?? 0) >= config.maxConcurrentJobsPerUser) {
132
+ throw new Error(
133
+ "Maximum concurrent background jobs reached"
134
+ );
135
+ }
136
+ raw.prepare(INSERT_JOB).run(
137
+ ...insertParameters(id, params, now)
138
+ );
139
+ return id;
140
+ }).immediate();
141
+ }
142
+ async getJob(jobId, userId) {
143
+ const row = await one(
144
+ `SELECT * FROM background_jobs WHERE id = ?${userId === "*" ? "" : " AND user_id = ?"}`,
145
+ ...[jobId, ...userId === "*" ? [] : [userId]]
146
+ );
147
+ return row ? toJob(row) : null;
148
+ }
149
+ async updateJob(jobId, update) {
150
+ const sets = ["last_activity_at = ?"];
151
+ const values = [Date.now()];
152
+ if (update.contentChunk !== void 0) {
153
+ sets.push("content = content || ?");
154
+ values.push(update.contentChunk);
155
+ }
156
+ if (update.chunksReceived !== void 0) {
157
+ sets.push("chunks_received = ?");
158
+ values.push(update.chunksReceived);
159
+ }
160
+ if (update.tool_calls !== void 0) {
161
+ sets.push("tool_calls_json = ?");
162
+ values.push(json(update.tool_calls));
163
+ }
164
+ if (update.workflow_state !== void 0) {
165
+ sets.push("workflow_state_json = ?");
166
+ values.push(json(update.workflow_state));
167
+ }
168
+ values.push(jobId);
169
+ let where = "id = ? AND status = 'streaming'";
170
+ if (update.leaseOwner) {
171
+ where += " AND lease_owner = ?";
172
+ values.push(update.leaseOwner);
173
+ }
174
+ const changes = await run(
175
+ `UPDATE background_jobs SET ${sets.join(", ")} WHERE ${where}`,
176
+ ...values
177
+ );
178
+ if (update.leaseOwner && changes === 0) throw leaseLost();
179
+ }
180
+ async completeJob(jobId, finalContent, leaseOwner) {
181
+ await this.finish(jobId, "complete", finalContent, null, leaseOwner);
182
+ }
183
+ async failJob(jobId, error, leaseOwner) {
184
+ await this.finish(jobId, "error", null, error, leaseOwner);
185
+ }
186
+ async finish(jobId, status, content, error, leaseOwner) {
187
+ const now = Date.now();
188
+ const values = [status, now, now, error];
189
+ const contentSet = content === null ? "" : ", content = ?";
190
+ if (content !== null) values.push(content);
191
+ values.push(jobId);
192
+ let where = "id = ? AND status = 'streaming'";
193
+ if (leaseOwner) {
194
+ where += " AND lease_owner = ?";
195
+ values.push(leaseOwner);
196
+ }
197
+ const changes = await run(
198
+ `UPDATE background_jobs SET status = ?, completed_at = ?, last_activity_at = ?,
199
+ error = ?, lease_owner = NULL, lease_expires_at = NULL${contentSet}
200
+ WHERE ${where}`,
201
+ ...values
202
+ );
203
+ if (leaseOwner && changes === 0) throw leaseLost();
204
+ }
205
+ async abortJob(jobId, userId) {
206
+ const now = Date.now();
207
+ return await run(
208
+ `UPDATE background_jobs SET status = 'aborted', completed_at = ?,
209
+ last_activity_at = ?, lease_owner = NULL, lease_expires_at = NULL
210
+ WHERE id = ? AND user_id = ? AND status = 'streaming'`,
211
+ now,
212
+ now,
213
+ jobId,
214
+ userId
215
+ ) > 0;
216
+ }
217
+ getAbortController() {
218
+ return void 0;
219
+ }
220
+ async checkJobAborted(jobId) {
221
+ const row = await one(
222
+ "SELECT status FROM background_jobs WHERE id = ?",
223
+ jobId
224
+ );
225
+ return row?.status === "aborted";
226
+ }
227
+ async claimJob(jobId, leaseOwner, now, leaseExpiresAt) {
228
+ const row = await one(
229
+ `UPDATE background_jobs SET lease_owner = ?, lease_expires_at = ?, attempts = attempts + 1
230
+ WHERE id = ? AND status = 'streaming' AND execution_json IS NOT NULL
231
+ AND (lease_owner IS NULL OR lease_expires_at <= ?)
232
+ RETURNING *`,
233
+ leaseOwner,
234
+ leaseExpiresAt,
235
+ jobId,
236
+ now
237
+ );
238
+ return row ? toJob(row) : null;
239
+ }
240
+ async claimNextJob(leaseOwner, now, leaseExpiresAt) {
241
+ const row = await one(
242
+ `UPDATE background_jobs SET lease_owner = ?, lease_expires_at = ?, attempts = attempts + 1
243
+ WHERE id = (
244
+ SELECT id FROM background_jobs
245
+ WHERE status = 'streaming' AND execution_json IS NOT NULL
246
+ AND (lease_owner IS NULL OR lease_expires_at <= ?)
247
+ ORDER BY started_at ASC LIMIT 1
248
+ ) RETURNING *`,
249
+ leaseOwner,
250
+ leaseExpiresAt,
251
+ now
252
+ );
253
+ return row ? toJob(row) : null;
254
+ }
255
+ async renewJobLease(jobId, leaseOwner, _now, leaseExpiresAt) {
256
+ return await run(
257
+ `UPDATE background_jobs SET lease_expires_at = ?, last_activity_at = ?
258
+ WHERE id = ? AND status = 'streaming' AND lease_owner = ?`,
259
+ leaseExpiresAt,
260
+ Date.now(),
261
+ jobId,
262
+ leaseOwner
263
+ ) > 0;
264
+ }
265
+ async updateJobExecution(jobId, execution, leaseOwner) {
266
+ return await run(
267
+ `UPDATE background_jobs SET execution_json = ?, last_activity_at = ?
268
+ WHERE id = ? AND status = 'streaming' AND lease_owner = ?`,
269
+ json(execution),
270
+ Date.now(),
271
+ jobId,
272
+ leaseOwner
273
+ ) > 0;
274
+ }
275
+ async cleanupExpired() {
276
+ const config = getJobConfig();
277
+ const now = Date.now();
278
+ const timedOut = await run(
279
+ `UPDATE background_jobs SET status = 'error', error = 'Job timed out',
280
+ completed_at = ?, lease_owner = NULL, lease_expires_at = NULL
281
+ WHERE status = 'streaming' AND last_activity_at <= ?`,
282
+ now,
283
+ now - config.jobTimeoutMs
284
+ );
285
+ const removed = await run(
286
+ `DELETE FROM background_jobs
287
+ WHERE status != 'streaming' AND completed_at IS NOT NULL AND completed_at <= ?`,
288
+ now - config.completedJobRetentionMs
289
+ );
290
+ return timedOut + removed;
291
+ }
292
+ async getActiveJobCount() {
293
+ const row = await one(
294
+ "SELECT COUNT(*) AS total FROM background_jobs WHERE status = 'streaming'"
295
+ );
296
+ return row?.total ?? 0;
297
+ }
298
+ }
299
+ export const sqliteBackgroundJobProvider = new SqliteBackgroundJobProvider();
@@ -15,6 +15,7 @@ import * as m013 from "./migrations/013_connect_environment_lifecycle.js";
15
15
  import * as m014 from "./migrations/014_connect_retention_activation.js";
16
16
  import * as m015 from "./migrations/015_rate_limits.js";
17
17
  import * as m016 from "./migrations/016_connect_runtime_binding.js";
18
+ import * as m017 from "./migrations/017_background_jobs.js";
18
19
  const REQUIRED_SCHEMA_TABLES = [
19
20
  "users",
20
21
  "auth_accounts",
@@ -39,7 +40,8 @@ const REQUIRED_SCHEMA_TABLES = [
39
40
  "upload_intents",
40
41
  "connect_device_authorizations",
41
42
  "connect_environments",
42
- "rate_limits"
43
+ "rate_limits",
44
+ "background_jobs"
43
45
  ];
44
46
  const migrations = {
45
47
  "001_init": m001,
@@ -57,7 +59,8 @@ const migrations = {
57
59
  "013_connect_environment_lifecycle": m013,
58
60
  "014_connect_retention_activation": m014,
59
61
  "015_rate_limits": m015,
60
- "016_connect_runtime_binding": m016
62
+ "016_connect_runtime_binding": m016,
63
+ "017_background_jobs": m017
61
64
  };
62
65
  class StaticMigrationProvider {
63
66
  async getMigrations() {
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Durable background chat and workflow execution state. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ await sql`
4
+ CREATE TABLE IF NOT EXISTS background_jobs (
5
+ id TEXT PRIMARY KEY,
6
+ user_id TEXT NOT NULL,
7
+ thread_id TEXT NOT NULL,
8
+ message_id TEXT NOT NULL,
9
+ model TEXT NOT NULL,
10
+ kind TEXT,
11
+ status TEXT NOT NULL,
12
+ content TEXT NOT NULL DEFAULT '',
13
+ chunks_received INTEGER NOT NULL DEFAULT 0,
14
+ started_at INTEGER NOT NULL,
15
+ last_activity_at INTEGER NOT NULL,
16
+ completed_at INTEGER,
17
+ error TEXT,
18
+ tool_calls_json TEXT,
19
+ workflow_state_json TEXT,
20
+ execution_json TEXT,
21
+ idempotency_key TEXT UNIQUE,
22
+ lease_owner TEXT,
23
+ lease_expires_at INTEGER,
24
+ attempts INTEGER NOT NULL DEFAULT 0
25
+ )
26
+ `.execute(db);
27
+ await sql`
28
+ CREATE INDEX IF NOT EXISTS background_jobs_active
29
+ ON background_jobs(status, last_activity_at)
30
+ `.execute(db);
31
+ await sql`
32
+ CREATE INDEX IF NOT EXISTS background_jobs_user_active
33
+ ON background_jobs(user_id, status)
34
+ `.execute(db);
35
+ await sql`
36
+ CREATE INDEX IF NOT EXISTS background_jobs_claim
37
+ ON background_jobs(status, lease_expires_at, started_at)
38
+ `.execute(db);
39
+ }
40
+ export async function down(db) {
41
+ await sql`DROP TABLE IF EXISTS background_jobs`.execute(db);
42
+ }
@@ -188,6 +188,28 @@ export interface RateLimitsTable {
188
188
  window_started_at: number;
189
189
  expires_at: number;
190
190
  }
191
+ export interface BackgroundJobsTable {
192
+ id: string;
193
+ user_id: string;
194
+ thread_id: string;
195
+ message_id: string;
196
+ model: string;
197
+ kind: string | null;
198
+ status: 'streaming' | 'complete' | 'error' | 'aborted';
199
+ content: string;
200
+ chunks_received: number;
201
+ started_at: number;
202
+ last_activity_at: number;
203
+ completed_at: number | null;
204
+ error: string | null;
205
+ tool_calls_json: string | null;
206
+ workflow_state_json: string | null;
207
+ execution_json: string | null;
208
+ idempotency_key: string | null;
209
+ lease_owner: string | null;
210
+ lease_expires_at: number | null;
211
+ attempts: number;
212
+ }
191
213
  export interface AdminUsersTable {
192
214
  user_id: string;
193
215
  created_at: number;
@@ -216,6 +238,7 @@ export interface Or3SqliteDb {
216
238
  connect_device_authorizations: ConnectDeviceAuthorizationsTable;
217
239
  connect_environments: ConnectEnvironmentsTable;
218
240
  rate_limits: RateLimitsTable;
241
+ background_jobs: BackgroundJobsTable;
219
242
  s_threads: SyncedEntityTable;
220
243
  s_messages: SyncedEntityTable;
221
244
  s_projects: SyncedEntityTable;
@@ -6,6 +6,7 @@ import { registerSyncGatewayAdapter } from "~~/server/sync/gateway/registry";
6
6
  import { registerWebhookStore } from "~~/server/utils/webhooks/store/registry";
7
7
  import { registerConnectStore } from "~~/server/connect/store/registry";
8
8
  import { registerRateLimitProvider } from "~~/server/utils/rate-limit/registry";
9
+ import { registerBackgroundJobProvider } from "~~/server/utils/background-jobs/registry";
9
10
  import { createSqliteAuthWorkspaceStore } from "../auth/sqlite-auth-workspace-store.js";
10
11
  import { createSqliteSyncGatewayAdapter } from "../sync/sqlite-sync-gateway-adapter.js";
11
12
  import { createSqliteWebhookStore } from "../webhooks/sqlite-webhook-store.js";
@@ -19,6 +20,7 @@ import { createSqliteConnectStore } from "../connect/sqlite-connect-store.js";
19
20
  import { getSqliteDriver, initializeSqliteDb } from "../db/kysely.js";
20
21
  import { runMigrations } from "../db/migrate.js";
21
22
  import { sqliteRateLimitProvider } from "../rate-limit/sqlite-provider.js";
23
+ import { sqliteBackgroundJobProvider } from "../background-jobs/sqlite-provider.js";
22
24
  import { useRuntimeConfig } from "#imports";
23
25
  const SQLITE_PROVIDER_ID = "sqlite";
24
26
  let d1RuntimeInitialization;
@@ -27,7 +29,8 @@ export default defineNitroPlugin(async (nitroApp) => {
27
29
  if (!config.auth?.enabled) return;
28
30
  const syncSelected = config.sync?.enabled === true && config.sync?.provider === SQLITE_PROVIDER_ID;
29
31
  const connectSelected = config.connect?.enabled === true && config.connect?.provider === SQLITE_PROVIDER_ID;
30
- if (!syncSelected && !connectSelected) return;
32
+ const backgroundJobsSelected = config.backgroundJobs?.enabled === true && config.backgroundJobs?.storageProvider === SQLITE_PROVIDER_ID;
33
+ if (!syncSelected && !connectSelected && !backgroundJobsSelected) return;
31
34
  const driver = getSqliteDriver();
32
35
  if (driver === "d1" && connectSelected) {
33
36
  throw new Error(
@@ -40,6 +43,10 @@ export default defineNitroPlugin(async (nitroApp) => {
40
43
  create: createSqliteAuthWorkspaceStore
41
44
  });
42
45
  registerRateLimitProvider(SQLITE_PROVIDER_ID, sqliteRateLimitProvider);
46
+ registerBackgroundJobProvider(
47
+ SQLITE_PROVIDER_ID,
48
+ sqliteBackgroundJobProvider
49
+ );
43
50
  if (syncSelected) {
44
51
  registerSyncGatewayAdapter({
45
52
  id: SQLITE_PROVIDER_ID,
@@ -21,6 +21,7 @@ const SNAPSHOT_TTL_SECONDS = 60 * 60;
21
21
  const SESSION_CONTEXT_KEY_PREFIX = "__or3_session_context_";
22
22
  const MIN_SYNC_RETENTION_SECONDS = 60 * 60;
23
23
  const MAX_SYNC_RETENTION_SECONDS = 365 * 24 * 60 * 60;
24
+ const MAX_SYNC_PAYLOAD_BYTES = 256 * 1024;
24
25
  function uid() {
25
26
  return globalThis.crypto.randomUUID();
26
27
  }
@@ -56,11 +57,15 @@ function validatePushOperation(workspaceId, op) {
56
57
  }
57
58
  const payload = op.payload;
58
59
  if (!payload) return void 0;
60
+ let serializedPayload;
59
61
  try {
60
- JSON.stringify(payload);
62
+ serializedPayload = JSON.stringify(payload);
61
63
  } catch {
62
64
  return `Invalid payload for ${op.tableName}: payload is not serializable`;
63
65
  }
66
+ if (new TextEncoder().encode(serializedPayload).byteLength > MAX_SYNC_PAYLOAD_BYTES) {
67
+ return `Payload too large for ${op.tableName}: exceeds ${MAX_SYNC_PAYLOAD_BYTES} bytes`;
68
+ }
64
69
  if ("_id" in payload) return `Invalid payload for ${op.tableName}: '_id' is immutable`;
65
70
  if ("workspace_id" in payload && payload.workspace_id !== workspaceId) {
66
71
  return `Invalid payload for ${op.tableName}: 'workspace_id' is immutable`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "or3-provider-sqlite",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Saluana/or3-provider-sqlite.git"