@absolutejs/sync 0.7.0 → 0.8.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,10 @@ 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, 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.
36
+ > permissions, live full-text + vector search, scheduled functions, a live
37
+ > devtools dashboard, CDC for Postgres/MySQL/SQLite, incremental aggregations +
38
+ > joins, and a declarative operator graph) are in place. Everything ships as
39
+ > subpaths of this one package.
39
40
 
40
41
  ## Install
41
42
 
@@ -332,13 +333,14 @@ it, ~3 store round-trips every 20ms ran the voice pipeline far slower than real
332
333
 
333
334
  ### `@absolutejs/sync`
334
335
 
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? })` _(`/scheduled` subpath)_ | Elysia plugin: fires the engine's registered schedules on their cron patterns (via `@elysiajs/cron`). Kept off the main entry so `syncSocket` needs no cron dep. |
341
- | `createWriteBehindCache({ load, persist, remove?, debounceMs?, evict?, onPersistError? })` | In-memory cache + write-behind persistence. |
336
+ | Export | What it is |
337
+ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
338
+ | `createReactiveHub()` | In-memory topic pub/sub (`publish`, `subscribe`, `subscriberCount`). |
339
+ | `sync({ hub, path?, resolveTopics?, heartbeatMs? })` | Elysia plugin: SSE stream of hub events. |
340
+ | `syncSocket({ engine, path?, resolveContext? })` | Elysia WebSocket plugin for the sync engine. |
341
+ | `scheduled({ engine, prefix?, onError? })` _(`/scheduled` subpath)_ | Elysia plugin: fires the engine's registered schedules on their cron patterns (via `@elysiajs/cron`). Kept off the main entry so `syncSocket` needs no cron dep. |
342
+ | `syncDevtools({ engine, path?, snapshotMs? })` | Elysia plugin: a live devtools dashboard (collections, subscription counts, mutations, schedules, change feed) over SSE. Backed by `engine.inspect()` + `engine.onActivity()`. |
343
+ | `createWriteBehindCache({ load, persist, remove?, debounceMs?, evict?, onPersistError? })` | In-memory cache + write-behind persistence. |
342
344
 
343
345
  ### `@absolutejs/sync/client`
344
346
 
@@ -0,0 +1,69 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { SyncEngine } from './engine/syncEngine';
3
+ export type SyncDevtoolsOptions = {
4
+ /** The engine to inspect. */
5
+ engine: SyncEngine;
6
+ /** Route the dashboard is served from (its SSE feed is `<path>/stream`). Default `/sync/devtools`. */
7
+ path?: string;
8
+ /** Snapshot refresh interval (ms) — keeps subscription counts/version current. Default 2000. */
9
+ snapshotMs?: number;
10
+ };
11
+ /**
12
+ * Elysia plugin: a live devtools dashboard for a {@link SyncEngine}. Mount it and
13
+ * open `path` in a browser to watch registered collections (kind, source tables,
14
+ * live subscription counts), mutations, schedules, readers/writers, the
15
+ * change-feed version, and a streaming log of changes + mutation outcomes — over
16
+ * Server-Sent Events. Read-only; safe to leave mounted in dev.
17
+ */
18
+ export declare const syncDevtools: ({ engine, path, snapshotMs }: SyncDevtoolsOptions) => Elysia<"", {
19
+ decorator: {};
20
+ store: {};
21
+ derive: {};
22
+ resolve: {};
23
+ }, {
24
+ typebox: {};
25
+ error: {};
26
+ }, {
27
+ schema: {};
28
+ standaloneSchema: {};
29
+ macro: {};
30
+ macroFn: {};
31
+ parser: {};
32
+ response: {};
33
+ }, {
34
+ [x: string]: {
35
+ get: {
36
+ body: unknown;
37
+ params: {};
38
+ query: unknown;
39
+ headers: unknown;
40
+ response: {
41
+ 200: Response;
42
+ };
43
+ };
44
+ };
45
+ } & {
46
+ [x: string]: {
47
+ get: {
48
+ body: unknown;
49
+ params: {};
50
+ query: unknown;
51
+ headers: unknown;
52
+ response: {
53
+ 200: Response;
54
+ };
55
+ };
56
+ };
57
+ }, {
58
+ derive: {};
59
+ resolve: {};
60
+ schema: {};
61
+ standaloneSchema: {};
62
+ response: {};
63
+ }, {
64
+ derive: {};
65
+ resolve: {};
66
+ schema: {};
67
+ standaloneSchema: {};
68
+ response: {};
69
+ }>;
@@ -0,0 +1,55 @@
1
+ import type { RowOp } from './types';
2
+ /**
3
+ * Devtools introspection — a live window into a running {@link SyncEngine}: what
4
+ * collections are registered, how many clients subscribe to each, which
5
+ * mutations/schedules/readers/writers exist, the change-feed version, and a tail
6
+ * of recent changes. Paired with the activity stream ({@link EngineActivity}) it
7
+ * powers the `syncDevtools` dashboard. Read-only — purely observational.
8
+ */
9
+ export type CollectionKind = 'view' | 'join' | 'graph' | 'reactive' | 'search';
10
+ /** One registered collection's current state. */
11
+ export type CollectionInspection = {
12
+ name: string;
13
+ kind: CollectionKind;
14
+ /** Source tables it reads (empty for a reactive query — its deps are dynamic). */
15
+ tables: string[];
16
+ /** Active client subscriptions to it right now. */
17
+ subscriptions: number;
18
+ };
19
+ /** A point-in-time snapshot of the engine (see {@link SyncEngine.inspect}). */
20
+ export type EngineInspection = {
21
+ /** Current change-feed version (monotonic). */
22
+ version: number;
23
+ collections: CollectionInspection[];
24
+ mutations: string[];
25
+ schedules: {
26
+ name: string;
27
+ pattern: string;
28
+ }[];
29
+ /** Tables with a registered reader / writer. */
30
+ readers: string[];
31
+ writers: string[];
32
+ /** Most recent changes from the change log (oldest first). */
33
+ recentChanges: {
34
+ version: number;
35
+ table: string;
36
+ op: RowOp;
37
+ }[];
38
+ };
39
+ /**
40
+ * A live engine event (see {@link SyncEngine.onActivity}): a committed change or
41
+ * a mutation outcome. `at` is `Date.now()`. (Live subscription counts come from
42
+ * the {@link EngineInspection} snapshot.)
43
+ */
44
+ export type EngineActivity = {
45
+ type: 'change';
46
+ at: number;
47
+ table: string;
48
+ op: RowOp;
49
+ version: number;
50
+ } | {
51
+ type: 'mutation';
52
+ at: number;
53
+ name: string;
54
+ status: 'ok' | 'error';
55
+ };
@@ -44,6 +44,7 @@ export { defineMutation } from './mutation';
44
44
  export type { MutationActions, MutationDefinition, MutationHandler, TableWriter, TransactionRunner } from './mutation';
45
45
  export { createSyncEngine, UnauthorizedError } from './syncEngine';
46
46
  export type { SubscribeArgs, Subscription, SyncEngine } from './syncEngine';
47
+ export type { CollectionInspection, CollectionKind, EngineActivity, EngineInspection } from './devtools';
47
48
  export { hydrateRoute, mutateRoute } from './routes';
48
49
  export type { SyncRouteContext } from './routes';
49
50
  export { createSyncConnection } from './connection';
@@ -1090,6 +1090,12 @@ var createSyncEngine = (options = {}) => {
1090
1090
  const changeLogSize = options.changeLogSize ?? 1024;
1091
1091
  const changeLog = [];
1092
1092
  let version = 0;
1093
+ const activityListeners = new Set;
1094
+ const emitActivity = (event) => {
1095
+ for (const listener of activityListeners) {
1096
+ listener(event);
1097
+ }
1098
+ };
1093
1099
  const runInTransaction = options.transaction;
1094
1100
  const instanceId = globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
1095
1101
  let clusterBus;
@@ -1412,6 +1418,13 @@ var createSyncEngine = (options = {}) => {
1412
1418
  version += 1;
1413
1419
  const changeVersion = version;
1414
1420
  logChange(changeVersion, { version: changeVersion, table, change });
1421
+ emitActivity({
1422
+ type: "change",
1423
+ at: Date.now(),
1424
+ table,
1425
+ op: change.op,
1426
+ version: changeVersion
1427
+ });
1415
1428
  const emissions = [];
1416
1429
  for (const subscription of subscriptionsForTable(table)) {
1417
1430
  const diff = await subscriptionDiff(subscription, table, change);
@@ -1440,6 +1453,13 @@ var createSyncEngine = (options = {}) => {
1440
1453
  const reactiveChanges = [];
1441
1454
  for (const { table, change } of changes) {
1442
1455
  logChange(batchVersion, { version: batchVersion, table, change });
1456
+ emitActivity({
1457
+ type: "change",
1458
+ at: Date.now(),
1459
+ table,
1460
+ op: change.op,
1461
+ version: batchVersion
1462
+ });
1443
1463
  reactiveChanges.push({
1444
1464
  table,
1445
1465
  key: changedKeyFor(table, change),
@@ -1816,13 +1836,29 @@ var createSyncEngine = (options = {}) => {
1816
1836
  }
1817
1837
  }
1818
1838
  const runHandler = async (tx) => {
1819
- const { actions, buffered: buffered2 } = makeActions(tx, ctx, true);
1820
- const result2 = await mutation.handler(args, ctx, actions);
1821
- return { buffered: buffered2, result: result2 };
1839
+ const { actions, buffered } = makeActions(tx, ctx, true);
1840
+ const result = await mutation.handler(args, ctx, actions);
1841
+ return { buffered, result };
1822
1842
  };
1823
- const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1824
- await applyChangeBatch(buffered);
1825
- return result;
1843
+ try {
1844
+ const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1845
+ await applyChangeBatch(buffered);
1846
+ emitActivity({
1847
+ type: "mutation",
1848
+ at: Date.now(),
1849
+ name,
1850
+ status: "ok"
1851
+ });
1852
+ return result;
1853
+ } catch (error) {
1854
+ emitActivity({
1855
+ type: "mutation",
1856
+ at: Date.now(),
1857
+ name,
1858
+ status: "error"
1859
+ });
1860
+ throw error;
1861
+ }
1826
1862
  },
1827
1863
  registerSchedule: (schedule) => {
1828
1864
  schedules.set(schedule.name, schedule);
@@ -1841,6 +1877,53 @@ var createSyncEngine = (options = {}) => {
1841
1877
  };
1842
1878
  const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1843
1879
  await applyChangeBatch(buffered);
1880
+ },
1881
+ inspect: () => {
1882
+ const collections = [...registry.entries()].map(([name, def]) => {
1883
+ const kind = def.kind ?? "view";
1884
+ let tables = [];
1885
+ if (kind === "join") {
1886
+ const join = def;
1887
+ tables = [join.left.table, join.right.table];
1888
+ } else if (kind === "graph") {
1889
+ tables = def.query.tables();
1890
+ } else if (kind === "search") {
1891
+ tables = [
1892
+ def.table
1893
+ ];
1894
+ } else if (kind === "view") {
1895
+ tables = def.tables ?? [name];
1896
+ }
1897
+ return {
1898
+ name,
1899
+ kind,
1900
+ tables,
1901
+ subscriptions: active.get(name)?.size ?? 0
1902
+ };
1903
+ });
1904
+ const DEVTOOLS_RECENT = 50;
1905
+ return {
1906
+ version,
1907
+ collections,
1908
+ mutations: [...mutations.keys()],
1909
+ schedules: [...schedules.values()].map((schedule) => ({
1910
+ name: schedule.name,
1911
+ pattern: schedule.pattern
1912
+ })),
1913
+ readers: [...readers.keys()],
1914
+ writers: [...writers.keys()],
1915
+ recentChanges: changeLog.slice(-DEVTOOLS_RECENT).map((entry) => ({
1916
+ version: entry.version,
1917
+ table: entry.table,
1918
+ op: entry.change.op
1919
+ }))
1920
+ };
1921
+ },
1922
+ onActivity: (listener) => {
1923
+ activityListeners.add(listener);
1924
+ return () => {
1925
+ activityListeners.delete(listener);
1926
+ };
1844
1927
  }
1845
1928
  };
1846
1929
  };
@@ -2115,5 +2198,5 @@ export {
2115
2198
  SEARCH_SCORE_FIELD
2116
2199
  };
2117
2200
 
2118
- //# debugId=54AD7964887E323764756E2164756E21
2201
+ //# debugId=B0B398375CBCC30B64756E2164756E21
2119
2202
  //# sourceMappingURL=index.js.map