@clivly/sdk 0.11.0 → 0.13.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.
@@ -1,4 +1,4 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-CvhxV-IX.cjs";
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-DK8mqmkV.cjs";
2
2
  //#region src/chat-session.d.ts
3
3
  export declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-CvhxV-IX.mjs";
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-DK8mqmkV.mjs";
2
2
  //#region src/chat-session.d.ts
3
3
  export declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
4
4
  //#endregion
package/dist/drizzle.cjs CHANGED
@@ -141,6 +141,7 @@ function fromDrizzle(db, table, opts) {
141
141
  const idColumnName = idColumn.name;
142
142
  const source = {
143
143
  entity: opts.entity,
144
+ ...opts.pushSource === void 0 ? {} : { pushSource: opts.pushSource },
144
145
  cursorField: cursorColumnName,
145
146
  idField: idColumnName,
146
147
  fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? drizzle_orm.sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy((0, drizzle_orm.asc)(cursorColumn), (0, drizzle_orm.asc)(idColumn)).limit(limit),
@@ -162,6 +163,69 @@ function fromDrizzle(db, table, opts) {
162
163
  return source;
163
164
  }
164
165
  /**
166
+ * Insert a row into a mapped Drizzle table and push it to Clivly immediately
167
+ * — the "one call does both" instant-push API (see
168
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md, question 1). Returns
169
+ * only after the write completes; the push itself is fire-and-forget (see
170
+ * `pushOne`), so this resolves at the same time a plain `db.insert(...)`
171
+ * would.
172
+ *
173
+ * Created once per mapped table, mirroring `fromDrizzle`'s own convention —
174
+ * no per-call table-to-entity lookup, so a table two entities share (e.g.
175
+ * `user` for both `user` and `organizationOwner`) is never ambiguous:
176
+ * ```ts
177
+ * const insertUser = insertAndPush(clivly, db, schema.user, { entity: "user" });
178
+ * // in the signup handler:
179
+ * const created = await insertUser(newUserValues, c.executionCtx);
180
+ * ```
181
+ *
182
+ * `waitUntil`, when provided (Hono exposes it as `c.executionCtx.waitUntil`),
183
+ * extends the request's lifetime past the response so the push survives on
184
+ * Cloudflare Workers and similar runtimes, which otherwise kill background
185
+ * work the instant a response is sent. Omit it on a long-lived process where
186
+ * nothing tears the request down early.
187
+ *
188
+ * Table property keys are translated to real database column names before
189
+ * the push — the same translation `fromDrizzle` performs on `fetchPage` rows
190
+ * — because a raw `.returning()` result is keyed by Drizzle property name
191
+ * (`fullName`), and every consumer downstream of a push (the fieldMap, the
192
+ * cloud's ingest) addresses columns by their database name (`full_name`).
193
+ * Skipping this would silently push empty fields with no error anywhere.
194
+ */
195
+ /**
196
+ * Translate a Drizzle-property-keyed row (e.g. a `.returning()` result, or a
197
+ * plain unprojected `select()`) to real database column names — the same
198
+ * translation `fromDrizzle` performs on `fetchPage` rows. Every consumer
199
+ * downstream of a push (the fieldMap, the cloud's ingest) addresses columns
200
+ * by their database name (`full_name`), not the property name (`fullName`);
201
+ * skipping this would silently push empty fields with no error anywhere.
202
+ *
203
+ * Exported so a host can build the same translated row from a table it read
204
+ * some other way — e.g. re-querying a row inside an auth library's own
205
+ * lifecycle hook, where there is no `insertAndPush` call site to wrap
206
+ * because the library performs the insert internally.
207
+ */
208
+ function toColumnKeyedRow(table, row) {
209
+ const columns = (0, drizzle_orm.getTableColumns)(table);
210
+ const columnNameByPropertyKey = /* @__PURE__ */ new Map();
211
+ for (const [propertyKey, column] of Object.entries(columns)) columnNameByPropertyKey.set(propertyKey, column.name);
212
+ const out = {};
213
+ for (const [propertyKey, value] of Object.entries(row)) out[columnNameByPropertyKey.get(propertyKey) ?? propertyKey] = value;
214
+ return out;
215
+ }
216
+ function insertAndPush(clivly, db, table, opts) {
217
+ const insertableDb = db;
218
+ return async (values, waitUntil) => {
219
+ const [created] = await insertableDb.insert(table).values(values).returning();
220
+ if (!created) throw new Error(`insertAndPush: insert into "${(0, drizzle_orm.getTableName)(table)}" returned no row.`);
221
+ const row = toColumnKeyedRow(table, created);
222
+ const pushed = clivly.pushOne(opts.entity, row);
223
+ if (waitUntil) waitUntil(pushed);
224
+ else await pushed;
225
+ return created;
226
+ };
227
+ }
228
+ /**
165
229
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
166
230
  * `.execute()`. Reads `information_schema` in the connection's default schema
167
231
  * (`current_schema()`), so it reports exactly the tables the app can reach.
@@ -206,3 +270,5 @@ Object.defineProperty(exports, "discoverFromDrizzle", {
206
270
  exports.drizzleIntrospector = drizzleIntrospector;
207
271
  exports.fromDrizzle = fromDrizzle;
208
272
  exports.fromDrizzleProjection = fromDrizzleProjection;
273
+ exports.insertAndPush = insertAndPush;
274
+ exports.toColumnKeyedRow = toColumnKeyedRow;
@@ -1,4 +1,4 @@
1
- import { _ as SyncSource, y as NamespaceIntrospector } from "./types-CvhxV-IX.cjs";
1
+ import { _ as SyncSource, y as NamespaceIntrospector } from "./types-DK8mqmkV.cjs";
2
2
  import { ProjectionMapping, ProjectionRef } from "@clivly/core";
3
3
  import { Table } from "drizzle-orm";
4
4
  import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
@@ -82,6 +82,21 @@ interface FromDrizzleOptions {
82
82
  * check and a false explanation.
83
83
  */
84
84
  objectType?: string;
85
+ /**
86
+ * The label this source's pushes carry, when it differs from `entity`.
87
+ *
88
+ * `entity` is the CURSOR KEY — two sources sharing it share a cursor, so the
89
+ * first to run advances it and the second silently pushes nothing. The push
90
+ * LABEL is matched against a mapping's `sourceTable`, which is the host
91
+ * table name. Those two requirements conflict whenever one table feeds two
92
+ * concepts: an `organization` table mapped as both a company and a
93
+ * subscription deal needs distinct cursors but the same table name on the
94
+ * wire.
95
+ *
96
+ * `fromDrizzleProjection` stamps this itself for the projection case. This
97
+ * is the same escape hatch for a plain single-table source.
98
+ */
99
+ pushSource?: string;
85
100
  }
86
101
  /**
87
102
  * Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
@@ -100,6 +115,56 @@ export declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizz
100
115
  objectType: string;
101
116
  };
102
117
  export declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions): SyncSource;
118
+ interface PushOneCapable {
119
+ pushOne(entityKey: string, row: Record<string, unknown>): Promise<void>;
120
+ }
121
+ /**
122
+ * Insert a row into a mapped Drizzle table and push it to Clivly immediately
123
+ * — the "one call does both" instant-push API (see
124
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md, question 1). Returns
125
+ * only after the write completes; the push itself is fire-and-forget (see
126
+ * `pushOne`), so this resolves at the same time a plain `db.insert(...)`
127
+ * would.
128
+ *
129
+ * Created once per mapped table, mirroring `fromDrizzle`'s own convention —
130
+ * no per-call table-to-entity lookup, so a table two entities share (e.g.
131
+ * `user` for both `user` and `organizationOwner`) is never ambiguous:
132
+ * ```ts
133
+ * const insertUser = insertAndPush(clivly, db, schema.user, { entity: "user" });
134
+ * // in the signup handler:
135
+ * const created = await insertUser(newUserValues, c.executionCtx);
136
+ * ```
137
+ *
138
+ * `waitUntil`, when provided (Hono exposes it as `c.executionCtx.waitUntil`),
139
+ * extends the request's lifetime past the response so the push survives on
140
+ * Cloudflare Workers and similar runtimes, which otherwise kill background
141
+ * work the instant a response is sent. Omit it on a long-lived process where
142
+ * nothing tears the request down early.
143
+ *
144
+ * Table property keys are translated to real database column names before
145
+ * the push — the same translation `fromDrizzle` performs on `fetchPage` rows
146
+ * — because a raw `.returning()` result is keyed by Drizzle property name
147
+ * (`fullName`), and every consumer downstream of a push (the fieldMap, the
148
+ * cloud's ingest) addresses columns by their database name (`full_name`).
149
+ * Skipping this would silently push empty fields with no error anywhere.
150
+ */
151
+ /**
152
+ * Translate a Drizzle-property-keyed row (e.g. a `.returning()` result, or a
153
+ * plain unprojected `select()`) to real database column names — the same
154
+ * translation `fromDrizzle` performs on `fetchPage` rows. Every consumer
155
+ * downstream of a push (the fieldMap, the cloud's ingest) addresses columns
156
+ * by their database name (`full_name`), not the property name (`fullName`);
157
+ * skipping this would silently push empty fields with no error anywhere.
158
+ *
159
+ * Exported so a host can build the same translated row from a table it read
160
+ * some other way — e.g. re-querying a row inside an auth library's own
161
+ * lifecycle hook, where there is no `insertAndPush` call site to wrap
162
+ * because the library performs the insert internally.
163
+ */
164
+ export declare function toColumnKeyedRow(table: Table, row: Record<string, unknown>): Record<string, unknown>;
165
+ export declare function insertAndPush(clivly: PushOneCapable, db: unknown, table: Table, opts: {
166
+ entity: string;
167
+ }): (values: Record<string, unknown>, waitUntil?: (promise: Promise<unknown>) => void) => Promise<Record<string, unknown>>;
103
168
  /**
104
169
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
105
170
  * `.execute()`. Reads `information_schema` in the connection's default schema
@@ -1,4 +1,4 @@
1
- import { _ as SyncSource, y as NamespaceIntrospector } from "./types-CvhxV-IX.mjs";
1
+ import { _ as SyncSource, y as NamespaceIntrospector } from "./types-DK8mqmkV.mjs";
2
2
  import { ProjectionMapping, ProjectionRef } from "@clivly/core";
3
3
  import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
4
4
  import { Table } from "drizzle-orm";
@@ -82,6 +82,21 @@ interface FromDrizzleOptions {
82
82
  * check and a false explanation.
83
83
  */
84
84
  objectType?: string;
85
+ /**
86
+ * The label this source's pushes carry, when it differs from `entity`.
87
+ *
88
+ * `entity` is the CURSOR KEY — two sources sharing it share a cursor, so the
89
+ * first to run advances it and the second silently pushes nothing. The push
90
+ * LABEL is matched against a mapping's `sourceTable`, which is the host
91
+ * table name. Those two requirements conflict whenever one table feeds two
92
+ * concepts: an `organization` table mapped as both a company and a
93
+ * subscription deal needs distinct cursors but the same table name on the
94
+ * wire.
95
+ *
96
+ * `fromDrizzleProjection` stamps this itself for the projection case. This
97
+ * is the same escape hatch for a plain single-table source.
98
+ */
99
+ pushSource?: string;
85
100
  }
86
101
  /**
87
102
  * Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
@@ -100,6 +115,56 @@ export declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizz
100
115
  objectType: string;
101
116
  };
102
117
  export declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions): SyncSource;
118
+ interface PushOneCapable {
119
+ pushOne(entityKey: string, row: Record<string, unknown>): Promise<void>;
120
+ }
121
+ /**
122
+ * Insert a row into a mapped Drizzle table and push it to Clivly immediately
123
+ * — the "one call does both" instant-push API (see
124
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md, question 1). Returns
125
+ * only after the write completes; the push itself is fire-and-forget (see
126
+ * `pushOne`), so this resolves at the same time a plain `db.insert(...)`
127
+ * would.
128
+ *
129
+ * Created once per mapped table, mirroring `fromDrizzle`'s own convention —
130
+ * no per-call table-to-entity lookup, so a table two entities share (e.g.
131
+ * `user` for both `user` and `organizationOwner`) is never ambiguous:
132
+ * ```ts
133
+ * const insertUser = insertAndPush(clivly, db, schema.user, { entity: "user" });
134
+ * // in the signup handler:
135
+ * const created = await insertUser(newUserValues, c.executionCtx);
136
+ * ```
137
+ *
138
+ * `waitUntil`, when provided (Hono exposes it as `c.executionCtx.waitUntil`),
139
+ * extends the request's lifetime past the response so the push survives on
140
+ * Cloudflare Workers and similar runtimes, which otherwise kill background
141
+ * work the instant a response is sent. Omit it on a long-lived process where
142
+ * nothing tears the request down early.
143
+ *
144
+ * Table property keys are translated to real database column names before
145
+ * the push — the same translation `fromDrizzle` performs on `fetchPage` rows
146
+ * — because a raw `.returning()` result is keyed by Drizzle property name
147
+ * (`fullName`), and every consumer downstream of a push (the fieldMap, the
148
+ * cloud's ingest) addresses columns by their database name (`full_name`).
149
+ * Skipping this would silently push empty fields with no error anywhere.
150
+ */
151
+ /**
152
+ * Translate a Drizzle-property-keyed row (e.g. a `.returning()` result, or a
153
+ * plain unprojected `select()`) to real database column names — the same
154
+ * translation `fromDrizzle` performs on `fetchPage` rows. Every consumer
155
+ * downstream of a push (the fieldMap, the cloud's ingest) addresses columns
156
+ * by their database name (`full_name`), not the property name (`fullName`);
157
+ * skipping this would silently push empty fields with no error anywhere.
158
+ *
159
+ * Exported so a host can build the same translated row from a table it read
160
+ * some other way — e.g. re-querying a row inside an auth library's own
161
+ * lifecycle hook, where there is no `insertAndPush` call site to wrap
162
+ * because the library performs the insert internally.
163
+ */
164
+ export declare function toColumnKeyedRow(table: Table, row: Record<string, unknown>): Record<string, unknown>;
165
+ export declare function insertAndPush(clivly: PushOneCapable, db: unknown, table: Table, opts: {
166
+ entity: string;
167
+ }): (values: Record<string, unknown>, waitUntil?: (promise: Promise<unknown>) => void) => Promise<Record<string, unknown>>;
103
168
  /**
104
169
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
105
170
  * `.execute()`. Reads `information_schema` in the connection's default schema
package/dist/drizzle.mjs CHANGED
@@ -140,6 +140,7 @@ function fromDrizzle(db, table, opts) {
140
140
  const idColumnName = idColumn.name;
141
141
  const source = {
142
142
  entity: opts.entity,
143
+ ...opts.pushSource === void 0 ? {} : { pushSource: opts.pushSource },
143
144
  cursorField: cursorColumnName,
144
145
  idField: idColumnName,
145
146
  fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy(asc(cursorColumn), asc(idColumn)).limit(limit),
@@ -161,6 +162,69 @@ function fromDrizzle(db, table, opts) {
161
162
  return source;
162
163
  }
163
164
  /**
165
+ * Insert a row into a mapped Drizzle table and push it to Clivly immediately
166
+ * — the "one call does both" instant-push API (see
167
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md, question 1). Returns
168
+ * only after the write completes; the push itself is fire-and-forget (see
169
+ * `pushOne`), so this resolves at the same time a plain `db.insert(...)`
170
+ * would.
171
+ *
172
+ * Created once per mapped table, mirroring `fromDrizzle`'s own convention —
173
+ * no per-call table-to-entity lookup, so a table two entities share (e.g.
174
+ * `user` for both `user` and `organizationOwner`) is never ambiguous:
175
+ * ```ts
176
+ * const insertUser = insertAndPush(clivly, db, schema.user, { entity: "user" });
177
+ * // in the signup handler:
178
+ * const created = await insertUser(newUserValues, c.executionCtx);
179
+ * ```
180
+ *
181
+ * `waitUntil`, when provided (Hono exposes it as `c.executionCtx.waitUntil`),
182
+ * extends the request's lifetime past the response so the push survives on
183
+ * Cloudflare Workers and similar runtimes, which otherwise kill background
184
+ * work the instant a response is sent. Omit it on a long-lived process where
185
+ * nothing tears the request down early.
186
+ *
187
+ * Table property keys are translated to real database column names before
188
+ * the push — the same translation `fromDrizzle` performs on `fetchPage` rows
189
+ * — because a raw `.returning()` result is keyed by Drizzle property name
190
+ * (`fullName`), and every consumer downstream of a push (the fieldMap, the
191
+ * cloud's ingest) addresses columns by their database name (`full_name`).
192
+ * Skipping this would silently push empty fields with no error anywhere.
193
+ */
194
+ /**
195
+ * Translate a Drizzle-property-keyed row (e.g. a `.returning()` result, or a
196
+ * plain unprojected `select()`) to real database column names — the same
197
+ * translation `fromDrizzle` performs on `fetchPage` rows. Every consumer
198
+ * downstream of a push (the fieldMap, the cloud's ingest) addresses columns
199
+ * by their database name (`full_name`), not the property name (`fullName`);
200
+ * skipping this would silently push empty fields with no error anywhere.
201
+ *
202
+ * Exported so a host can build the same translated row from a table it read
203
+ * some other way — e.g. re-querying a row inside an auth library's own
204
+ * lifecycle hook, where there is no `insertAndPush` call site to wrap
205
+ * because the library performs the insert internally.
206
+ */
207
+ function toColumnKeyedRow(table, row) {
208
+ const columns = getTableColumns(table);
209
+ const columnNameByPropertyKey = /* @__PURE__ */ new Map();
210
+ for (const [propertyKey, column] of Object.entries(columns)) columnNameByPropertyKey.set(propertyKey, column.name);
211
+ const out = {};
212
+ for (const [propertyKey, value] of Object.entries(row)) out[columnNameByPropertyKey.get(propertyKey) ?? propertyKey] = value;
213
+ return out;
214
+ }
215
+ function insertAndPush(clivly, db, table, opts) {
216
+ const insertableDb = db;
217
+ return async (values, waitUntil) => {
218
+ const [created] = await insertableDb.insert(table).values(values).returning();
219
+ if (!created) throw new Error(`insertAndPush: insert into "${getTableName(table)}" returned no row.`);
220
+ const row = toColumnKeyedRow(table, created);
221
+ const pushed = clivly.pushOne(opts.entity, row);
222
+ if (waitUntil) waitUntil(pushed);
223
+ else await pushed;
224
+ return created;
225
+ };
226
+ }
227
+ /**
164
228
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
165
229
  * `.execute()`. Reads `information_schema` in the connection's default schema
166
230
  * (`current_schema()`), so it reports exactly the tables the app can reach.
@@ -190,4 +254,4 @@ function drizzleIntrospector(db) {
190
254
  };
191
255
  }
192
256
  //#endregion
193
- export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection };
257
+ export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection, insertAndPush, toColumnKeyedRow };
package/dist/index.cjs CHANGED
@@ -701,6 +701,20 @@ function detectHostEnvironment(env, configured) {
701
701
  return fromNodeEnv(env.NODE_ENV?.trim());
702
702
  }
703
703
  //#endregion
704
+ //#region src/instant-push-support.ts
705
+ /**
706
+ * The fast rollback switch for instant push (see
707
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md, question 5).
708
+ *
709
+ * A host-side env var, not a Clivly-Cloud-side toggle: it must keep working
710
+ * even when Clivly Cloud is the thing misbehaving (e.g. its ingest endpoint
711
+ * is what instant push is flooding), and it must take effect without a
712
+ * redeploy — just an env var change and the next cold start.
713
+ */
714
+ function isInstantPushDisabled(env) {
715
+ return env.CLIVLY_INSTANT_PUSH_DISABLED === "true";
716
+ }
717
+ //#endregion
704
718
  //#region src/presence-machine.ts
705
719
  const PRESENCE_BACKOFF_BASE_MS = 1e3;
706
720
  const PRESENCE_BACKOFF_MAX_MS = 3e4;
@@ -1972,6 +1986,28 @@ function createClivlySDK(config) {
1972
1986
  lastPushFailure = null;
1973
1987
  if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
1974
1988
  };
1989
+ const pushOne = async (entityKey, row) => {
1990
+ if (config.sync?.instant !== true) return;
1991
+ if (isInstantPushDisabled(config.env ?? process.env)) return;
1992
+ const resolved = resolveEntitySource(config, entityKey);
1993
+ if (!resolved) {
1994
+ console.warn(`Clivly instant push: no source is configured for entity "${entityKey}" — this row was not pushed. It will still sync on the next delta run once \`entities\` declares it.`);
1995
+ return;
1996
+ }
1997
+ const path = CONCEPT_PATHS[resolved.expectedKey];
1998
+ if (!path) return;
1999
+ const push = createPush();
2000
+ try {
2001
+ if (!await push(path, {
2002
+ source: pushSourceOf(resolved.source),
2003
+ rows: [row],
2004
+ mode: "delta",
2005
+ ...resolved.expectedKey === "custom" ? { objectType: resolved.source.objectType } : {}
2006
+ })) console.warn(`Clivly instant push failed for "${entityKey}". This row will still sync on the next delta run.`);
2007
+ } catch (error) {
2008
+ console.warn(`Clivly instant push failed for "${entityKey}": ${error instanceof Error ? error.message : String(error)}. This row will still sync on the next delta run.`);
2009
+ }
2010
+ };
1975
2011
  const describeSyncTrigger = () => ({
1976
2012
  configured: config.syncTrigger?.url,
1977
2013
  resolved: resolveTriggerUrl({
@@ -2327,7 +2363,8 @@ function createClivlySDK(config) {
2327
2363
  runScheduled,
2328
2364
  createClivlyHandler,
2329
2365
  sync: runSync,
2330
- createAuthVerifyHandler
2366
+ createAuthVerifyHandler,
2367
+ pushOne
2331
2368
  };
2332
2369
  }
2333
2370
  //#endregion
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-CvhxV-IX.cjs";
1
+ import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-DK8mqmkV.cjs";
2
2
  import { createChatSessionHandler } from "./chat-session.cjs";
3
3
  import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
4
4
  //#region src/trigger-url.d.ts
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-CvhxV-IX.mjs";
1
+ import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-DK8mqmkV.mjs";
2
2
  import { createChatSessionHandler } from "./chat-session.mjs";
3
3
  import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
4
4
  //#region src/trigger-url.d.ts
package/dist/index.mjs CHANGED
@@ -700,6 +700,20 @@ function detectHostEnvironment(env, configured) {
700
700
  return fromNodeEnv(env.NODE_ENV?.trim());
701
701
  }
702
702
  //#endregion
703
+ //#region src/instant-push-support.ts
704
+ /**
705
+ * The fast rollback switch for instant push (see
706
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md, question 5).
707
+ *
708
+ * A host-side env var, not a Clivly-Cloud-side toggle: it must keep working
709
+ * even when Clivly Cloud is the thing misbehaving (e.g. its ingest endpoint
710
+ * is what instant push is flooding), and it must take effect without a
711
+ * redeploy — just an env var change and the next cold start.
712
+ */
713
+ function isInstantPushDisabled(env) {
714
+ return env.CLIVLY_INSTANT_PUSH_DISABLED === "true";
715
+ }
716
+ //#endregion
703
717
  //#region src/presence-machine.ts
704
718
  const PRESENCE_BACKOFF_BASE_MS = 1e3;
705
719
  const PRESENCE_BACKOFF_MAX_MS = 3e4;
@@ -1971,6 +1985,28 @@ function createClivlySDK(config) {
1971
1985
  lastPushFailure = null;
1972
1986
  if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
1973
1987
  };
1988
+ const pushOne = async (entityKey, row) => {
1989
+ if (config.sync?.instant !== true) return;
1990
+ if (isInstantPushDisabled(config.env ?? process.env)) return;
1991
+ const resolved = resolveEntitySource(config, entityKey);
1992
+ if (!resolved) {
1993
+ console.warn(`Clivly instant push: no source is configured for entity "${entityKey}" — this row was not pushed. It will still sync on the next delta run once \`entities\` declares it.`);
1994
+ return;
1995
+ }
1996
+ const path = CONCEPT_PATHS[resolved.expectedKey];
1997
+ if (!path) return;
1998
+ const push = createPush();
1999
+ try {
2000
+ if (!await push(path, {
2001
+ source: pushSourceOf(resolved.source),
2002
+ rows: [row],
2003
+ mode: "delta",
2004
+ ...resolved.expectedKey === "custom" ? { objectType: resolved.source.objectType } : {}
2005
+ })) console.warn(`Clivly instant push failed for "${entityKey}". This row will still sync on the next delta run.`);
2006
+ } catch (error) {
2007
+ console.warn(`Clivly instant push failed for "${entityKey}": ${error instanceof Error ? error.message : String(error)}. This row will still sync on the next delta run.`);
2008
+ }
2009
+ };
1974
2010
  const describeSyncTrigger = () => ({
1975
2011
  configured: config.syncTrigger?.url,
1976
2012
  resolved: resolveTriggerUrl({
@@ -2326,7 +2362,8 @@ function createClivlySDK(config) {
2326
2362
  runScheduled,
2327
2363
  createClivlyHandler,
2328
2364
  sync: runSync,
2329
- createAuthVerifyHandler
2365
+ createAuthVerifyHandler,
2366
+ pushOne
2330
2367
  };
2331
2368
  }
2332
2369
  //#endregion
@@ -208,6 +208,23 @@ interface ClivlySDKConfig {
208
208
  */
209
209
  intervalMs?: number;
210
210
  batchSize?: number;
211
+ /**
212
+ * Opt into `pushOne()`/`insertAndPush()` pushing a single changed row to
213
+ * Clivly immediately, instead of waiting for the next delta sync. Off by
214
+ * default: turning it on changes a write's async failure surface (a push
215
+ * can fail independently of the write that triggered it, swallowed and
216
+ * left for the next delta sync to catch), which an existing install
217
+ * upgrading to a newer `clivly` should opt into deliberately rather than
218
+ * inherit silently. Requires a real scheduled delta sync running
219
+ * underneath as the safety net for anything the instant push misses (a
220
+ * failed push, a raw SQL write, a bulk import) — see
221
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md.
222
+ *
223
+ * Can also be disabled at runtime without a redeploy via the
224
+ * `CLIVLY_INSTANT_PUSH_DISABLED=true` environment variable — the fast
225
+ * rollback switch if this misbehaves in production.
226
+ */
227
+ instant?: boolean;
211
228
  };
212
229
  /**
213
230
  * The remote sync trigger: how Clivly Cloud asks this app to run a sync.
@@ -521,6 +538,31 @@ interface ClivlySDK {
521
538
  * own state.
522
539
  */
523
540
  ping(): Promise<ClivlyConnectResult>;
541
+ /**
542
+ * Push one row to Clivly immediately, instead of waiting for the next delta
543
+ * sync — the "instant" half of near-real-time sync (see
544
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md). No-ops unless
545
+ * `sync.instant` is set and `CLIVLY_INSTANT_PUSH_DISABLED` is unset.
546
+ *
547
+ * `entityKey` is a key of `config.entities.entities` (the same key
548
+ * `clivly.config.ts` declares the entity under), not a table name — an
549
+ * unresolvable key logs a warning and returns without throwing, the same
550
+ * fail-safe as every other call site here: a config mismatch never syncs
551
+ * the wrong thing, it just doesn't sync this row instantly (the safety-net
552
+ * delta sync still will).
553
+ *
554
+ * `row` must be keyed by the source table's real database column names
555
+ * (`full_name`, not `fullName`) — the same contract `fetchPage` rows
556
+ * satisfy. `insertAndPush` (from `clivly/drizzle`) performs this
557
+ * translation for you from a Drizzle table; calling `pushOne` directly with
558
+ * a raw Drizzle `.returning()` row will silently push empty fields.
559
+ *
560
+ * Never throws and never advances the delta cursor. Fire-and-forget by
561
+ * design: on Cloudflare Workers and similar runtimes, wrap the call in
562
+ * `ctx.waitUntil(...)` so it survives past the response, since nothing
563
+ * here blocks or awaits the caller's own write.
564
+ */
565
+ pushOne(entityKey: string, row: Record<string, unknown>): Promise<void>;
524
566
  /**
525
567
  * One-shot heartbeat + a single sync, with no keep-alive timers. The correct
526
568
  * entry point on serverless/edge (Workers, Vercel, Lambda), where `start()`'s
@@ -208,6 +208,23 @@ interface ClivlySDKConfig {
208
208
  */
209
209
  intervalMs?: number;
210
210
  batchSize?: number;
211
+ /**
212
+ * Opt into `pushOne()`/`insertAndPush()` pushing a single changed row to
213
+ * Clivly immediately, instead of waiting for the next delta sync. Off by
214
+ * default: turning it on changes a write's async failure surface (a push
215
+ * can fail independently of the write that triggered it, swallowed and
216
+ * left for the next delta sync to catch), which an existing install
217
+ * upgrading to a newer `clivly` should opt into deliberately rather than
218
+ * inherit silently. Requires a real scheduled delta sync running
219
+ * underneath as the safety net for anything the instant push misses (a
220
+ * failed push, a raw SQL write, a bulk import) — see
221
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md.
222
+ *
223
+ * Can also be disabled at runtime without a redeploy via the
224
+ * `CLIVLY_INSTANT_PUSH_DISABLED=true` environment variable — the fast
225
+ * rollback switch if this misbehaves in production.
226
+ */
227
+ instant?: boolean;
211
228
  };
212
229
  /**
213
230
  * The remote sync trigger: how Clivly Cloud asks this app to run a sync.
@@ -521,6 +538,31 @@ interface ClivlySDK {
521
538
  * own state.
522
539
  */
523
540
  ping(): Promise<ClivlyConnectResult>;
541
+ /**
542
+ * Push one row to Clivly immediately, instead of waiting for the next delta
543
+ * sync — the "instant" half of near-real-time sync (see
544
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md). No-ops unless
545
+ * `sync.instant` is set and `CLIVLY_INSTANT_PUSH_DISABLED` is unset.
546
+ *
547
+ * `entityKey` is a key of `config.entities.entities` (the same key
548
+ * `clivly.config.ts` declares the entity under), not a table name — an
549
+ * unresolvable key logs a warning and returns without throwing, the same
550
+ * fail-safe as every other call site here: a config mismatch never syncs
551
+ * the wrong thing, it just doesn't sync this row instantly (the safety-net
552
+ * delta sync still will).
553
+ *
554
+ * `row` must be keyed by the source table's real database column names
555
+ * (`full_name`, not `fullName`) — the same contract `fetchPage` rows
556
+ * satisfy. `insertAndPush` (from `clivly/drizzle`) performs this
557
+ * translation for you from a Drizzle table; calling `pushOne` directly with
558
+ * a raw Drizzle `.returning()` row will silently push empty fields.
559
+ *
560
+ * Never throws and never advances the delta cursor. Fire-and-forget by
561
+ * design: on Cloudflare Workers and similar runtimes, wrap the call in
562
+ * `ctx.waitUntil(...)` so it survives past the response, since nothing
563
+ * here blocks or awaits the caller's own write.
564
+ */
565
+ pushOne(entityKey: string, row: Record<string, unknown>): Promise<void>;
524
566
  /**
525
567
  * One-shot heartbeat + a single sync, with no keep-alive timers. The correct
526
568
  * entry point on serverless/edge (Workers, Vercel, Lambda), where `start()`'s
package/dist/vite.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlySDK } from "./types-CvhxV-IX.cjs";
1
+ import { a as ClivlySDK } from "./types-DK8mqmkV.cjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/vite.d.ts
4
4
  export interface ClivlyViteOptions {
package/dist/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlySDK } from "./types-CvhxV-IX.mjs";
1
+ import { a as ClivlySDK } from "./types-DK8mqmkV.mjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/vite.d.ts
4
4
  export interface ClivlyViteOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clivly/sdk",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "private": false,
5
5
  "description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
6
6
  "license": "MIT",
@@ -53,7 +53,7 @@
53
53
  "README.md"
54
54
  ],
55
55
  "dependencies": {
56
- "@clivly/core": "0.11.0"
56
+ "@clivly/core": "0.13.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@electric-sql/pglite": "^0.5.4",