@absolutejs/sync 0.5.0 → 0.6.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/README.md CHANGED
@@ -33,9 +33,9 @@ top-N ordering are maintained incrementally through a composable operator graph
33
33
  > write-behind cache), Tier 2 (Drizzle + Prisma topic adapters, `createLiveQuery`),
34
34
  > and Tier 3 (sync engine: collections, WebSocket diff transport, optimistic
35
35
  > mutations + offline queue, a local-first client cache, declarative row-level
36
- > permissions, live full-text + vector search, CDC for Postgres/MySQL/SQLite,
37
- > incremental aggregations + joins, and a declarative operator graph) are in
38
- > place. Everything ships as subpaths of this one package.
36
+ > permissions, live full-text + vector search, scheduled functions, CDC for
37
+ > Postgres/MySQL/SQLite, incremental aggregations + joins, and a declarative
38
+ > operator graph) are in place. Everything ships as subpaths of this one package.
39
39
 
40
40
  ## Install
41
41
 
@@ -280,6 +280,32 @@ await orders.mutate({
280
280
  });
281
281
  ```
282
282
 
283
+ - **Scheduled functions.** Register server-side work that runs on a cron pattern;
284
+ whatever it writes via `ctx.actions` goes live through the change feed (and it can
285
+ read current state via `ctx.db`). Cron decides _when_ (via `@elysiajs/cron`, an
286
+ optional peer); the engine makes the effect _live_. It doesn't reinvent jobs —
287
+ for durable, retryable work a schedule can `enqueue` into
288
+ [`@absolutejs/queue`](https://github.com/absolutejs/queue).
289
+
290
+ ```ts
291
+ import { scheduled } from '@absolutejs/sync';
292
+
293
+ engine.registerSchedule({
294
+ name: 'digest',
295
+ pattern: '0 8 * * 1', // Mondays 08:00 (6-field for seconds: '*/5 * * * * *')
296
+ run: async ({ db, actions }) => {
297
+ const stale = await db.all('reports');
298
+ await actions.insert('digests', {
299
+ id: crypto.randomUUID(),
300
+ at: Date.now()
301
+ });
302
+ // or: queue.enqueue('email.send', { … }) for durable delivery
303
+ }
304
+ });
305
+
306
+ new Elysia().use(syncSocket({ engine })).use(scheduled({ engine })); // wires cron
307
+ ```
308
+
283
309
  ## Write-behind cache — keep a remote store off your hot path
284
310
 
285
311
  ```ts
@@ -306,12 +332,13 @@ it, ~3 store round-trips every 20ms ran the voice pipeline far slower than real
306
332
 
307
333
  ### `@absolutejs/sync`
308
334
 
309
- | Export | What it is |
310
- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
311
- | `createReactiveHub()` | In-memory topic pub/sub (`publish`, `subscribe`, `subscriberCount`). |
312
- | `sync({ hub, path?, resolveTopics?, heartbeatMs? })` | Elysia plugin: SSE stream of hub events. |
313
- | `syncSocket({ engine, path?, resolveContext? })` | Elysia WebSocket plugin for the sync engine. |
314
- | `createWriteBehindCache({ load, persist, remove?, debounceMs?, evict?, onPersistError? })` | In-memory cache + write-behind persistence. |
335
+ | Export | What it is |
336
+ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
337
+ | `createReactiveHub()` | In-memory topic pub/sub (`publish`, `subscribe`, `subscriberCount`). |
338
+ | `sync({ hub, path?, resolveTopics?, heartbeatMs? })` | Elysia plugin: SSE stream of hub events. |
339
+ | `syncSocket({ engine, path?, resolveContext? })` | Elysia WebSocket plugin for the sync engine. |
340
+ | `scheduled({ engine, prefix?, onError? })` | Elysia plugin: fires the engine's registered schedules on their cron patterns (via `@elysiajs/cron`). |
341
+ | `createWriteBehindCache({ load, persist, remove?, debounceMs?, evict?, onPersistError? })` | In-memory cache + write-behind persistence. |
315
342
 
316
343
  ### `@absolutejs/sync/client`
317
344
 
@@ -377,6 +404,7 @@ mutate({
377
404
  | `defineSearchCollection({ name, table, index, source, key, limit? })` + `registerSearch` | Live search collection: the subscription's `params` are the query (string/vector), the ranked top-K stream back as a normal collection, re-ranked as rows change. Each row carries its score under `_score`. |
378
405
  | `createTextIndex({ key, fields, tokenize?, stopwords?, k1?, b? })` | Incremental BM25 full-text index (keyword search). Implements `SearchIndex`; usable standalone or inside a search collection. |
379
406
  | `createVectorIndex({ key, embedding, metric? })` | Incremental vector index (cosine/dot/euclidean exact k-NN) for semantic search — pairs with `@absolutejs/ai` / `@absolutejs/rag` for RAG retrieval on your own data. |
407
+ | `defineSchedule({ name, pattern, run })` + `registerSchedule` / `runSchedule` | Scheduled function: `run({ db, actions })` fires on a cron `pattern`; its writes go live through the change feed. Wire triggers with the `scheduled` plugin (or call `runSchedule(name)` on demand). |
380
408
 
381
409
  ### `@absolutejs/sync/postgres`
382
410
 
@@ -38,6 +38,8 @@ export { createTextIndex } from './textIndex';
38
38
  export type { TextIndexOptions } from './textIndex';
39
39
  export { createVectorIndex } from './vectorIndex';
40
40
  export type { VectorIndexOptions, VectorMetric } from './vectorIndex';
41
+ export { defineSchedule } from './schedule';
42
+ export type { ScheduleContext, ScheduleDefinition } from './schedule';
41
43
  export { defineMutation } from './mutation';
42
44
  export type { MutationActions, MutationDefinition, MutationHandler, TableWriter, TransactionRunner } from './mutation';
43
45
  export { createSyncEngine, UnauthorizedError } from './syncEngine';
@@ -1035,6 +1035,8 @@ var createVectorIndex = (options) => {
1035
1035
  }
1036
1036
  };
1037
1037
  };
1038
+ // src/engine/schedule.ts
1039
+ var defineSchedule = (definition) => definition;
1038
1040
  // src/engine/mutation.ts
1039
1041
  var defineMutation = (definition) => definition;
1040
1042
  // src/engine/syncEngine.ts
@@ -1070,6 +1072,7 @@ var createSyncEngine = (options = {}) => {
1070
1072
  const mutations = new Map;
1071
1073
  const writers = new Map;
1072
1074
  const readers = new Map;
1075
+ const schedules = new Map;
1073
1076
  const permissions = new Map;
1074
1077
  for (const [table, rules] of Object.entries(options.permissions ?? {})) {
1075
1078
  permissions.set(table, rules);
@@ -1201,7 +1204,7 @@ var createSyncEngine = (options = {}) => {
1201
1204
  };
1202
1205
  const depKey = (table, key) => `${table} ${key}`;
1203
1206
  const changedKeyFor = (table, change) => readers.get(table)?.key?.(change.row);
1204
- const makeReadHandle = (ctx, readTables, readKeys, rangeDeps) => {
1207
+ const makeReadHandle = (ctx, readTables, readKeys, rangeDeps, applyRules = true) => {
1205
1208
  const readerFor = (table) => {
1206
1209
  const reader = readers.get(table);
1207
1210
  if (reader === undefined) {
@@ -1209,11 +1212,12 @@ var createSyncEngine = (options = {}) => {
1209
1212
  }
1210
1213
  return reader;
1211
1214
  };
1215
+ const ruleFor = (table) => applyRules ? readRuleFor(table) : undefined;
1212
1216
  return {
1213
1217
  all: async (table) => {
1214
1218
  readTables.add(table);
1215
1219
  const rows = [...await readerFor(table).all(ctx)];
1216
- const rule = readRuleFor(table);
1220
+ const rule = ruleFor(table);
1217
1221
  return rule ? rows.filter((row) => rule(ctx, row)) : rows;
1218
1222
  },
1219
1223
  get: async (table, key) => {
@@ -1227,12 +1231,12 @@ var createSyncEngine = (options = {}) => {
1227
1231
  readTables.add(table);
1228
1232
  }
1229
1233
  const row = await reader.get(key, ctx);
1230
- const rule = readRuleFor(table);
1234
+ const rule = ruleFor(table);
1231
1235
  return rule && row !== undefined && !rule(ctx, row) ? undefined : row;
1232
1236
  },
1233
1237
  where: async (table, predicate) => {
1234
1238
  const reader = readerFor(table);
1235
- const rule = readRuleFor(table);
1239
+ const rule = ruleFor(table);
1236
1240
  const effective = rule ? (row) => predicate(row) && rule(ctx, row) : predicate;
1237
1241
  const matched = [...await reader.all(ctx)].filter(effective);
1238
1242
  if (reader.key !== undefined) {
@@ -1249,6 +1253,71 @@ var createSyncEngine = (options = {}) => {
1249
1253
  }
1250
1254
  };
1251
1255
  };
1256
+ const writerFor = (table) => {
1257
+ const writer = writers.get(table);
1258
+ if (writer === undefined) {
1259
+ throw new Error(`No writer registered for table "${table}" \u2014 register one with engine.registerWriter, or use actions.change`);
1260
+ }
1261
+ return writer;
1262
+ };
1263
+ const authorizeWrite = async (table, op, value, ctx) => {
1264
+ const rule = writeRuleFor(table, op);
1265
+ if (rule === undefined) {
1266
+ return;
1267
+ }
1268
+ let subject = value;
1269
+ if (op !== "insert") {
1270
+ const reader = readers.get(table);
1271
+ if (reader?.get !== undefined) {
1272
+ const id = reader.key ? reader.key(value) : value.id;
1273
+ if (id !== undefined) {
1274
+ const existing = await reader.get(id, ctx);
1275
+ if (existing !== undefined) {
1276
+ subject = existing;
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+ if (!rule(ctx, subject)) {
1282
+ throw new UnauthorizedError(`${op} on table "${table}"`);
1283
+ }
1284
+ };
1285
+ const makeActions = (tx, ctx, enforce) => {
1286
+ const buffered = [];
1287
+ const actions = {
1288
+ change: (collection, change) => {
1289
+ buffered.push({
1290
+ table: collection,
1291
+ change
1292
+ });
1293
+ return Promise.resolve();
1294
+ },
1295
+ insert: async (table, data) => {
1296
+ if (enforce) {
1297
+ await authorizeWrite(table, "insert", data, ctx);
1298
+ }
1299
+ const row = await writerFor(table).insert(data, ctx, tx);
1300
+ buffered.push({ table, change: { op: "insert", row } });
1301
+ return row;
1302
+ },
1303
+ update: async (table, data) => {
1304
+ if (enforce) {
1305
+ await authorizeWrite(table, "update", data, ctx);
1306
+ }
1307
+ const row = await writerFor(table).update(data, ctx, tx);
1308
+ buffered.push({ table, change: { op: "update", row } });
1309
+ return row;
1310
+ },
1311
+ delete: async (table, row) => {
1312
+ if (enforce) {
1313
+ await authorizeWrite(table, "delete", row, ctx);
1314
+ }
1315
+ await writerFor(table).delete(row, ctx, tx);
1316
+ buffered.push({ table, change: { op: "delete", row } });
1317
+ }
1318
+ };
1319
+ return { actions, buffered };
1320
+ };
1252
1321
  const diffRerun = (sub, rows, equals = shallowEqual4) => {
1253
1322
  const next = new Map;
1254
1323
  for (const row of rows) {
@@ -1746,69 +1815,32 @@ var createSyncEngine = (options = {}) => {
1746
1815
  throw new UnauthorizedError(`run mutation "${name}"`);
1747
1816
  }
1748
1817
  }
1749
- const writerFor = (table) => {
1750
- const writer = writers.get(table);
1751
- if (writer === undefined) {
1752
- throw new Error(`No writer registered for table "${table}" \u2014 register one with engine.registerWriter, or use actions.change`);
1753
- }
1754
- return writer;
1755
- };
1756
- const authorizeWrite = async (table, op, value) => {
1757
- const rule = writeRuleFor(table, op);
1758
- if (rule === undefined) {
1759
- return;
1760
- }
1761
- let subject = value;
1762
- if (op !== "insert") {
1763
- const reader = readers.get(table);
1764
- if (reader?.get !== undefined) {
1765
- const id = reader.key ? reader.key(value) : value.id;
1766
- if (id !== undefined) {
1767
- const existing = await reader.get(id, ctx);
1768
- if (existing !== undefined) {
1769
- subject = existing;
1770
- }
1771
- }
1772
- }
1773
- }
1774
- if (!rule(ctx, subject)) {
1775
- throw new UnauthorizedError(`${op} on table "${table}"`);
1776
- }
1777
- };
1778
1818
  const runHandler = async (tx) => {
1779
- const buffered2 = [];
1780
- const actions = {
1781
- change: (collection, change) => {
1782
- buffered2.push({
1783
- table: collection,
1784
- change
1785
- });
1786
- return Promise.resolve();
1787
- },
1788
- insert: async (table, data) => {
1789
- await authorizeWrite(table, "insert", data);
1790
- const row = await writerFor(table).insert(data, ctx, tx);
1791
- buffered2.push({ table, change: { op: "insert", row } });
1792
- return row;
1793
- },
1794
- update: async (table, data) => {
1795
- await authorizeWrite(table, "update", data);
1796
- const row = await writerFor(table).update(data, ctx, tx);
1797
- buffered2.push({ table, change: { op: "update", row } });
1798
- return row;
1799
- },
1800
- delete: async (table, row) => {
1801
- await authorizeWrite(table, "delete", row);
1802
- await writerFor(table).delete(row, ctx, tx);
1803
- buffered2.push({ table, change: { op: "delete", row } });
1804
- }
1805
- };
1806
- const handlerResult = await mutation.handler(args, ctx, actions);
1807
- return { buffered: buffered2, result: handlerResult };
1819
+ const { actions, buffered: buffered2 } = makeActions(tx, ctx, true);
1820
+ const result2 = await mutation.handler(args, ctx, actions);
1821
+ return { buffered: buffered2, result: result2 };
1808
1822
  };
1809
1823
  const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1810
1824
  await applyChangeBatch(buffered);
1811
1825
  return result;
1826
+ },
1827
+ registerSchedule: (schedule) => {
1828
+ schedules.set(schedule.name, schedule);
1829
+ },
1830
+ listSchedules: () => [...schedules.values()],
1831
+ runSchedule: async (name) => {
1832
+ const schedule = schedules.get(name);
1833
+ if (schedule === undefined) {
1834
+ throw new Error(`Unknown schedule "${name}"`);
1835
+ }
1836
+ const runHandler = async (tx) => {
1837
+ const { actions, buffered: buffered2 } = makeActions(tx, {}, false);
1838
+ const db = makeReadHandle({}, new Set, new Set, [], false);
1839
+ await schedule.run({ actions, db });
1840
+ return buffered2;
1841
+ };
1842
+ const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1843
+ await applyChangeBatch(buffered);
1812
1844
  }
1813
1845
  };
1814
1846
  };
@@ -2060,6 +2092,7 @@ export {
2060
2092
  fromRowChange,
2061
2093
  filterOp,
2062
2094
  defineSearchCollection,
2095
+ defineSchedule,
2063
2096
  defineReactiveQuery,
2064
2097
  definePermissions,
2065
2098
  defineMutation,
@@ -2082,5 +2115,5 @@ export {
2082
2115
  SEARCH_SCORE_FIELD
2083
2116
  };
2084
2117
 
2085
- //# debugId=28E08370F43FEBAE64756E2164756E21
2118
+ //# debugId=54AD7964887E323764756E2164756E21
2086
2119
  //# sourceMappingURL=index.js.map