clivly 0.12.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.
package/drizzle.cjs CHANGED
@@ -164,6 +164,69 @@ function fromDrizzle(db, table, opts) {
164
164
  return source;
165
165
  }
166
166
  /**
167
+ * Insert a row into a mapped Drizzle table and push it to Clivly immediately
168
+ * — the "one call does both" instant-push API (see
169
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md, question 1). Returns
170
+ * only after the write completes; the push itself is fire-and-forget (see
171
+ * `pushOne`), so this resolves at the same time a plain `db.insert(...)`
172
+ * would.
173
+ *
174
+ * Created once per mapped table, mirroring `fromDrizzle`'s own convention —
175
+ * no per-call table-to-entity lookup, so a table two entities share (e.g.
176
+ * `user` for both `user` and `organizationOwner`) is never ambiguous:
177
+ * ```ts
178
+ * const insertUser = insertAndPush(clivly, db, schema.user, { entity: "user" });
179
+ * // in the signup handler:
180
+ * const created = await insertUser(newUserValues, c.executionCtx);
181
+ * ```
182
+ *
183
+ * `waitUntil`, when provided (Hono exposes it as `c.executionCtx.waitUntil`),
184
+ * extends the request's lifetime past the response so the push survives on
185
+ * Cloudflare Workers and similar runtimes, which otherwise kill background
186
+ * work the instant a response is sent. Omit it on a long-lived process where
187
+ * nothing tears the request down early.
188
+ *
189
+ * Table property keys are translated to real database column names before
190
+ * the push — the same translation `fromDrizzle` performs on `fetchPage` rows
191
+ * — because a raw `.returning()` result is keyed by Drizzle property name
192
+ * (`fullName`), and every consumer downstream of a push (the fieldMap, the
193
+ * cloud's ingest) addresses columns by their database name (`full_name`).
194
+ * Skipping this would silently push empty fields with no error anywhere.
195
+ */
196
+ /**
197
+ * Translate a Drizzle-property-keyed row (e.g. a `.returning()` result, or a
198
+ * plain unprojected `select()`) to real database column names — the same
199
+ * translation `fromDrizzle` performs on `fetchPage` rows. Every consumer
200
+ * downstream of a push (the fieldMap, the cloud's ingest) addresses columns
201
+ * by their database name (`full_name`), not the property name (`fullName`);
202
+ * skipping this would silently push empty fields with no error anywhere.
203
+ *
204
+ * Exported so a host can build the same translated row from a table it read
205
+ * some other way — e.g. re-querying a row inside an auth library's own
206
+ * lifecycle hook, where there is no `insertAndPush` call site to wrap
207
+ * because the library performs the insert internally.
208
+ */
209
+ function toColumnKeyedRow(table, row) {
210
+ const columns = (0, drizzle_orm.getTableColumns)(table);
211
+ const columnNameByPropertyKey = /* @__PURE__ */ new Map();
212
+ for (const [propertyKey, column] of Object.entries(columns)) columnNameByPropertyKey.set(propertyKey, column.name);
213
+ const out = {};
214
+ for (const [propertyKey, value] of Object.entries(row)) out[columnNameByPropertyKey.get(propertyKey) ?? propertyKey] = value;
215
+ return out;
216
+ }
217
+ function insertAndPush(clivly, db, table, opts) {
218
+ const insertableDb = db;
219
+ return async (values, waitUntil) => {
220
+ const [created] = await insertableDb.insert(table).values(values).returning();
221
+ if (!created) throw new Error(`insertAndPush: insert into "${(0, drizzle_orm.getTableName)(table)}" returned no row.`);
222
+ const row = toColumnKeyedRow(table, created);
223
+ const pushed = clivly.pushOne(opts.entity, row);
224
+ if (waitUntil) waitUntil(pushed);
225
+ else await pushed;
226
+ return created;
227
+ };
228
+ }
229
+ /**
167
230
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
168
231
  * `.execute()`. Reads `information_schema` in the connection's default schema
169
232
  * (`current_schema()`), so it reports exactly the tables the app can reach.
@@ -198,3 +261,5 @@ exports.discoverFromDrizzle = require_drizzle.discoverFromDrizzle;
198
261
  exports.drizzleIntrospector = drizzleIntrospector;
199
262
  exports.fromDrizzle = fromDrizzle;
200
263
  exports.fromDrizzleProjection = fromDrizzleProjection;
264
+ exports.insertAndPush = insertAndPush;
265
+ exports.toColumnKeyedRow = toColumnKeyedRow;
package/drizzle.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { R as ProjectionMapping, z as ProjectionRef } from "./entity-config-ApnT5j3X.cjs";
2
2
  import { n as buildUniqueConstraints, r as discoverFromDrizzle } from "./drizzle-CiK37zJm.cjs";
3
- import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-C5RLl74M.cjs";
3
+ import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-H7yWc0JV.cjs";
4
4
  import { Table } from "drizzle-orm";
5
5
  //#region ../sdk/src/drizzle-projection.d.ts
6
6
  /**
@@ -115,6 +115,56 @@ export declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizz
115
115
  objectType: string;
116
116
  };
117
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>>;
118
168
  /**
119
169
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
120
170
  * `.execute()`. Reads `information_schema` in the connection's default schema
package/drizzle.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { R as ProjectionMapping, z as ProjectionRef } from "./entity-config-CKcj4-M_.mjs";
2
2
  import { n as buildUniqueConstraints, r as discoverFromDrizzle } from "./drizzle-ehVWLSrN.mjs";
3
- import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-CNQKSmxz.mjs";
3
+ import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-DykR0kgh.mjs";
4
4
  import { Table } from "drizzle-orm";
5
5
  //#region ../sdk/src/drizzle-projection.d.ts
6
6
  /**
@@ -115,6 +115,56 @@ export declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizz
115
115
  objectType: string;
116
116
  };
117
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>>;
118
168
  /**
119
169
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
120
170
  * `.execute()`. Reads `information_schema` in the connection's default schema
package/drizzle.mjs CHANGED
@@ -163,6 +163,69 @@ function fromDrizzle(db, table, opts) {
163
163
  return source;
164
164
  }
165
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 = 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 "${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
+ /**
166
229
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
167
230
  * `.execute()`. Reads `information_schema` in the connection's default schema
168
231
  * (`current_schema()`), so it reports exactly the tables the app can reach.
@@ -192,4 +255,4 @@ function drizzleIntrospector(db) {
192
255
  };
193
256
  }
194
257
  //#endregion
195
- export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection };
258
+ export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection, insertAndPush, toColumnKeyedRow };
package/index.cjs CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_entity_config = require("./entity-config-BI4RYNKL.cjs");
4
4
  require("./mapping-score-CaB3F4Uu.cjs");
5
5
  const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
6
- const require_sdk = require("./sdk-BvGSxBQc.cjs");
6
+ const require_sdk = require("./sdk-DKfKhJD_.cjs");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_fs = require("node:fs");
9
9
  let node_module = require("node:module");
package/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { t as CANONICAL_FIELDS } from "./entity-config-Ccfb2IL9.mjs";
3
3
  import "./mapping-score-C68uAqGB.mjs";
4
4
  import { hasDefiningColumn, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
5
- import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-C-Pjxj2y.mjs";
5
+ import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-B-dp1tZN.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { spawn } from "node:child_process";
8
8
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
@@ -164,6 +164,23 @@ interface ClivlySDKConfig {
164
164
  */
165
165
  intervalMs?: number;
166
166
  batchSize?: number;
167
+ /**
168
+ * Opt into `pushOne()`/`insertAndPush()` pushing a single changed row to
169
+ * Clivly immediately, instead of waiting for the next delta sync. Off by
170
+ * default: turning it on changes a write's async failure surface (a push
171
+ * can fail independently of the write that triggered it, swallowed and
172
+ * left for the next delta sync to catch), which an existing install
173
+ * upgrading to a newer `clivly` should opt into deliberately rather than
174
+ * inherit silently. Requires a real scheduled delta sync running
175
+ * underneath as the safety net for anything the instant push misses (a
176
+ * failed push, a raw SQL write, a bulk import) — see
177
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md.
178
+ *
179
+ * Can also be disabled at runtime without a redeploy via the
180
+ * `CLIVLY_INSTANT_PUSH_DISABLED=true` environment variable — the fast
181
+ * rollback switch if this misbehaves in production.
182
+ */
183
+ instant?: boolean;
167
184
  };
168
185
  /**
169
186
  * The remote sync trigger: how Clivly Cloud asks this app to run a sync.
@@ -477,6 +494,31 @@ interface ClivlySDK {
477
494
  * own state.
478
495
  */
479
496
  ping(): Promise<ClivlyConnectResult>;
497
+ /**
498
+ * Push one row to Clivly immediately, instead of waiting for the next delta
499
+ * sync — the "instant" half of near-real-time sync (see
500
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md). No-ops unless
501
+ * `sync.instant` is set and `CLIVLY_INSTANT_PUSH_DISABLED` is unset.
502
+ *
503
+ * `entityKey` is a key of `config.entities.entities` (the same key
504
+ * `clivly.config.ts` declares the entity under), not a table name — an
505
+ * unresolvable key logs a warning and returns without throwing, the same
506
+ * fail-safe as every other call site here: a config mismatch never syncs
507
+ * the wrong thing, it just doesn't sync this row instantly (the safety-net
508
+ * delta sync still will).
509
+ *
510
+ * `row` must be keyed by the source table's real database column names
511
+ * (`full_name`, not `fullName`) — the same contract `fetchPage` rows
512
+ * satisfy. `insertAndPush` (from `clivly/drizzle`) performs this
513
+ * translation for you from a Drizzle table; calling `pushOne` directly with
514
+ * a raw Drizzle `.returning()` row will silently push empty fields.
515
+ *
516
+ * Never throws and never advances the delta cursor. Fire-and-forget by
517
+ * design: on Cloudflare Workers and similar runtimes, wrap the call in
518
+ * `ctx.waitUntil(...)` so it survives past the response, since nothing
519
+ * here blocks or awaits the caller's own write.
520
+ */
521
+ pushOne(entityKey: string, row: Record<string, unknown>): Promise<void>;
480
522
  /**
481
523
  * One-shot heartbeat + a single sync, with no keep-alive timers. The correct
482
524
  * entry point on serverless/edge (Workers, Vercel, Lambda), where `start()`'s
@@ -164,6 +164,23 @@ interface ClivlySDKConfig {
164
164
  */
165
165
  intervalMs?: number;
166
166
  batchSize?: number;
167
+ /**
168
+ * Opt into `pushOne()`/`insertAndPush()` pushing a single changed row to
169
+ * Clivly immediately, instead of waiting for the next delta sync. Off by
170
+ * default: turning it on changes a write's async failure surface (a push
171
+ * can fail independently of the write that triggered it, swallowed and
172
+ * left for the next delta sync to catch), which an existing install
173
+ * upgrading to a newer `clivly` should opt into deliberately rather than
174
+ * inherit silently. Requires a real scheduled delta sync running
175
+ * underneath as the safety net for anything the instant push misses (a
176
+ * failed push, a raw SQL write, a bulk import) — see
177
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md.
178
+ *
179
+ * Can also be disabled at runtime without a redeploy via the
180
+ * `CLIVLY_INSTANT_PUSH_DISABLED=true` environment variable — the fast
181
+ * rollback switch if this misbehaves in production.
182
+ */
183
+ instant?: boolean;
167
184
  };
168
185
  /**
169
186
  * The remote sync trigger: how Clivly Cloud asks this app to run a sync.
@@ -477,6 +494,31 @@ interface ClivlySDK {
477
494
  * own state.
478
495
  */
479
496
  ping(): Promise<ClivlyConnectResult>;
497
+ /**
498
+ * Push one row to Clivly immediately, instead of waiting for the next delta
499
+ * sync — the "instant" half of near-real-time sync (see
500
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md). No-ops unless
501
+ * `sync.instant` is set and `CLIVLY_INSTANT_PUSH_DISABLED` is unset.
502
+ *
503
+ * `entityKey` is a key of `config.entities.entities` (the same key
504
+ * `clivly.config.ts` declares the entity under), not a table name — an
505
+ * unresolvable key logs a warning and returns without throwing, the same
506
+ * fail-safe as every other call site here: a config mismatch never syncs
507
+ * the wrong thing, it just doesn't sync this row instantly (the safety-net
508
+ * delta sync still will).
509
+ *
510
+ * `row` must be keyed by the source table's real database column names
511
+ * (`full_name`, not `fullName`) — the same contract `fetchPage` rows
512
+ * satisfy. `insertAndPush` (from `clivly/drizzle`) performs this
513
+ * translation for you from a Drizzle table; calling `pushOne` directly with
514
+ * a raw Drizzle `.returning()` row will silently push empty fields.
515
+ *
516
+ * Never throws and never advances the delta cursor. Fire-and-forget by
517
+ * design: on Cloudflare Workers and similar runtimes, wrap the call in
518
+ * `ctx.waitUntil(...)` so it survives past the response, since nothing
519
+ * here blocks or awaits the caller's own write.
520
+ */
521
+ pushOne(entityKey: string, row: Record<string, unknown>): Promise<void>;
480
522
  /**
481
523
  * One-shot heartbeat + a single sync, with no keep-alive timers. The correct
482
524
  * entry point on serverless/edge (Workers, Vercel, Lambda), where `start()`'s
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clivly",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly CLI — scaffold a CRM into your app (clivly init).",
@@ -699,6 +699,20 @@ function detectHostEnvironment(env, configured) {
699
699
  return fromNodeEnv(env.NODE_ENV?.trim());
700
700
  }
701
701
  //#endregion
702
+ //#region ../sdk/src/instant-push-support.ts
703
+ /**
704
+ * The fast rollback switch for instant push (see
705
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md, question 5).
706
+ *
707
+ * A host-side env var, not a Clivly-Cloud-side toggle: it must keep working
708
+ * even when Clivly Cloud is the thing misbehaving (e.g. its ingest endpoint
709
+ * is what instant push is flooding), and it must take effect without a
710
+ * redeploy — just an env var change and the next cold start.
711
+ */
712
+ function isInstantPushDisabled(env) {
713
+ return env.CLIVLY_INSTANT_PUSH_DISABLED === "true";
714
+ }
715
+ //#endregion
702
716
  //#region ../sdk/src/presence-machine.ts
703
717
  const PRESENCE_BACKOFF_BASE_MS = 1e3;
704
718
  const PRESENCE_BACKOFF_MAX_MS = 3e4;
@@ -2090,6 +2104,28 @@ function createClivlySDK(config) {
2090
2104
  lastPushFailure = null;
2091
2105
  if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
2092
2106
  };
2107
+ const pushOne = async (entityKey, row) => {
2108
+ if (config.sync?.instant !== true) return;
2109
+ if (isInstantPushDisabled(config.env ?? process.env)) return;
2110
+ const resolved = resolveEntitySource(config, entityKey);
2111
+ if (!resolved) {
2112
+ 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.`);
2113
+ return;
2114
+ }
2115
+ const path = CONCEPT_PATHS[resolved.expectedKey];
2116
+ if (!path) return;
2117
+ const push = createPush();
2118
+ try {
2119
+ if (!await push(path, {
2120
+ source: pushSourceOf(resolved.source),
2121
+ rows: [row],
2122
+ mode: "delta",
2123
+ ...resolved.expectedKey === "custom" ? { objectType: resolved.source.objectType } : {}
2124
+ })) console.warn(`Clivly instant push failed for "${entityKey}". This row will still sync on the next delta run.`);
2125
+ } catch (error) {
2126
+ 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.`);
2127
+ }
2128
+ };
2093
2129
  const describeSyncTrigger = () => ({
2094
2130
  configured: config.syncTrigger?.url,
2095
2131
  resolved: resolveTriggerUrl({
@@ -2445,7 +2481,8 @@ function createClivlySDK(config) {
2445
2481
  runScheduled,
2446
2482
  createClivlyHandler,
2447
2483
  sync: runSync,
2448
- createAuthVerifyHandler
2484
+ createAuthVerifyHandler,
2485
+ pushOne
2449
2486
  };
2450
2487
  }
2451
2488
  //#endregion
@@ -699,6 +699,20 @@ function detectHostEnvironment(env, configured) {
699
699
  return fromNodeEnv(env.NODE_ENV?.trim());
700
700
  }
701
701
  //#endregion
702
+ //#region ../sdk/src/instant-push-support.ts
703
+ /**
704
+ * The fast rollback switch for instant push (see
705
+ * docs/reviews/2026-09-19-near-real-time-sync-plan.md, question 5).
706
+ *
707
+ * A host-side env var, not a Clivly-Cloud-side toggle: it must keep working
708
+ * even when Clivly Cloud is the thing misbehaving (e.g. its ingest endpoint
709
+ * is what instant push is flooding), and it must take effect without a
710
+ * redeploy — just an env var change and the next cold start.
711
+ */
712
+ function isInstantPushDisabled(env) {
713
+ return env.CLIVLY_INSTANT_PUSH_DISABLED === "true";
714
+ }
715
+ //#endregion
702
716
  //#region ../sdk/src/presence-machine.ts
703
717
  const PRESENCE_BACKOFF_BASE_MS = 1e3;
704
718
  const PRESENCE_BACKOFF_MAX_MS = 3e4;
@@ -2090,6 +2104,28 @@ function createClivlySDK(config) {
2090
2104
  lastPushFailure = null;
2091
2105
  if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
2092
2106
  };
2107
+ const pushOne = async (entityKey, row) => {
2108
+ if (config.sync?.instant !== true) return;
2109
+ if (isInstantPushDisabled(config.env ?? process.env)) return;
2110
+ const resolved = resolveEntitySource(config, entityKey);
2111
+ if (!resolved) {
2112
+ 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.`);
2113
+ return;
2114
+ }
2115
+ const path = CONCEPT_PATHS[resolved.expectedKey];
2116
+ if (!path) return;
2117
+ const push = createPush();
2118
+ try {
2119
+ if (!await push(path, {
2120
+ source: pushSourceOf(resolved.source),
2121
+ rows: [row],
2122
+ mode: "delta",
2123
+ ...resolved.expectedKey === "custom" ? { objectType: resolved.source.objectType } : {}
2124
+ })) console.warn(`Clivly instant push failed for "${entityKey}". This row will still sync on the next delta run.`);
2125
+ } catch (error) {
2126
+ 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.`);
2127
+ }
2128
+ };
2093
2129
  const describeSyncTrigger = () => ({
2094
2130
  configured: config.syncTrigger?.url,
2095
2131
  resolved: resolveTriggerUrl({
@@ -2445,7 +2481,8 @@ function createClivlySDK(config) {
2445
2481
  runScheduled,
2446
2482
  createClivlyHandler,
2447
2483
  sync: runSync,
2448
- createAuthVerifyHandler
2484
+ createAuthVerifyHandler,
2485
+ pushOne
2449
2486
  };
2450
2487
  }
2451
2488
  //#endregion
package/sdk.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_entity_config = require("./entity-config-BI4RYNKL.cjs");
3
- const require_sdk = require("./sdk-BvGSxBQc.cjs");
3
+ const require_sdk = require("./sdk-DKfKhJD_.cjs");
4
4
  exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
5
5
  exports.ClivlyError = require_sdk.ClivlyError;
6
6
  exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
package/sdk.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as customObjectDeclarations, M as validateEntitiesConfig, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult, w as defineClivlyConfig } from "./entity-config-ApnT5j3X.cjs";
2
- import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-C5RLl74M.cjs";
2
+ import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-H7yWc0JV.cjs";
3
3
  //#region ../sdk/src/trigger-url.d.ts
4
4
  export declare function isPlaceholderUrl(value: string): boolean;
5
5
  //#endregion
package/sdk.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as customObjectDeclarations, M as validateEntitiesConfig, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult, w as defineClivlyConfig } from "./entity-config-CKcj4-M_.mjs";
2
- import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-CNQKSmxz.mjs";
2
+ import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-DykR0kgh.mjs";
3
3
  //#region ../sdk/src/trigger-url.d.ts
4
4
  export declare function isPlaceholderUrl(value: string): boolean;
5
5
  //#endregion
package/sdk.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { c as defineClivlyConfig, g as validateEntitiesConfig, s as customObjectDeclarations } from "./entity-config-Ccfb2IL9.mjs";
2
- import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-C-Pjxj2y.mjs";
2
+ import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-B-dp1tZN.mjs";
3
3
  export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
package/vite.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as ClivlySDK } from "./namespace-probe-C5RLl74M.cjs";
1
+ import { o as ClivlySDK } from "./namespace-probe-H7yWc0JV.cjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region ../sdk/src/vite.d.ts
4
4
  export interface ClivlyViteOptions {
package/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as ClivlySDK } from "./namespace-probe-CNQKSmxz.mjs";
1
+ import { o as ClivlySDK } from "./namespace-probe-DykR0kgh.mjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region ../sdk/src/vite.d.ts
4
4
  export interface ClivlyViteOptions {