@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 +37 -9
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.js +96 -63
- package/dist/engine/index.js.map +5 -4
- package/dist/engine/schedule.d.ts +39 -0
- package/dist/engine/syncEngine.d.ts +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -1
- package/dist/index.js.map +4 -3
- package/dist/scheduled.d.ts +47 -0
- package/package.json +6 -1
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
|
|
37
|
-
> incremental aggregations + joins, and a declarative
|
|
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
|
-
| `
|
|
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
|
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/engine/index.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1781
|
-
|
|
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=
|
|
2118
|
+
//# debugId=54AD7964887E323764756E2164756E21
|
|
2086
2119
|
//# sourceMappingURL=index.js.map
|