@absolutejs/sync 0.0.1 → 0.2.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 +281 -24
- package/dist/adapters/drizzle/collection.d.ts +27 -0
- package/dist/adapters/drizzle/index.d.ts +20 -0
- package/dist/adapters/drizzle/index.js +265 -0
- package/dist/adapters/drizzle/index.js.map +14 -0
- package/dist/adapters/drizzle/predicate.d.ts +20 -0
- package/dist/adapters/drizzle/read.d.ts +31 -0
- package/dist/adapters/drizzle/topics.d.ts +41 -0
- package/dist/adapters/drizzle/write.d.ts +69 -0
- package/dist/adapters/mysql/index.d.ts +75 -0
- package/dist/adapters/mysql/index.js +171 -0
- package/dist/adapters/mysql/index.js.map +11 -0
- package/dist/adapters/postgres/index.d.ts +53 -0
- package/dist/adapters/postgres/index.js +86 -0
- package/dist/adapters/postgres/index.js.map +10 -0
- package/dist/adapters/prisma/collection.d.ts +39 -0
- package/dist/adapters/prisma/index.d.ts +23 -0
- package/dist/adapters/prisma/index.js +231 -0
- package/dist/adapters/prisma/index.js.map +14 -0
- package/dist/adapters/prisma/predicate.d.ts +20 -0
- package/dist/adapters/prisma/read.d.ts +28 -0
- package/dist/adapters/prisma/topics.d.ts +29 -0
- package/dist/adapters/prisma/write.d.ts +65 -0
- package/dist/adapters/sqlite/index.d.ts +32 -0
- package/dist/adapters/sqlite/index.js +128 -0
- package/dist/adapters/sqlite/index.js.map +11 -0
- package/dist/angular/index.d.ts +1 -0
- package/dist/angular/index.js +347 -0
- package/dist/angular/index.js.map +11 -0
- package/dist/angular/sync-collection.service.d.ts +20 -0
- package/dist/client/index.d.ts +12 -30
- package/dist/client/index.js +1099 -3
- package/dist/client/index.js.map +10 -4
- package/dist/client/liveQuery.d.ts +75 -0
- package/dist/client/presence.d.ts +37 -0
- package/dist/client/subscriber.d.ts +30 -0
- package/dist/client/syncClient.d.ts +53 -0
- package/dist/client/syncCollection.d.ts +102 -0
- package/dist/client/syncStore.d.ts +81 -0
- package/dist/engine/aggregate.d.ts +45 -0
- package/dist/engine/cluster.d.ts +41 -0
- package/dist/engine/collection.d.ts +87 -0
- package/dist/engine/connection.d.ts +103 -0
- package/dist/engine/dataflow.d.ts +109 -0
- package/dist/engine/equiJoin.d.ts +51 -0
- package/dist/engine/graph.d.ts +85 -0
- package/dist/engine/index.d.ts +40 -0
- package/dist/engine/index.js +1774 -0
- package/dist/engine/index.js.map +23 -0
- package/dist/engine/materializedView.d.ts +53 -0
- package/dist/engine/mutation.d.ts +66 -0
- package/dist/engine/pollingSource.d.ts +42 -0
- package/dist/engine/presence.d.ts +46 -0
- package/dist/engine/reactive.d.ts +67 -0
- package/dist/engine/routes.d.ts +40 -0
- package/dist/engine/socket.d.ts +67 -0
- package/dist/engine/syncEngine.d.ts +132 -0
- package/dist/engine/types.d.ts +45 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +327 -3
- package/dist/index.js.map +8 -5
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +332 -0
- package/dist/react/index.js.map +11 -0
- package/dist/react/useSyncCollection.d.ts +16 -0
- package/dist/reactiveHub.d.ts +6 -0
- package/dist/svelte/createSyncCollectionStore.d.ts +15 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +338 -0
- package/dist/svelte/index.js.map +11 -0
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +331 -0
- package/dist/vue/index.js.map +11 -0
- package/dist/vue/useSyncCollection.d.ts +17 -0
- package/package.json +104 -6
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RowChange, RowKey } from './types';
|
|
2
|
+
export type AggregateOptions<T> = {
|
|
3
|
+
/** Row identity — used to track each row's contribution across updates. */
|
|
4
|
+
key: (row: T) => RowKey;
|
|
5
|
+
/** Group rows by this key. Omit to aggregate everything into one group (`''`). */
|
|
6
|
+
groupBy?: (row: T) => RowKey;
|
|
7
|
+
/**
|
|
8
|
+
* Numeric value to aggregate for `sum`/`avg`/`min`/`max`. Omit for a
|
|
9
|
+
* count-only aggregate (sum stays 0, min/max stay undefined).
|
|
10
|
+
*/
|
|
11
|
+
value?: (row: T) => number;
|
|
12
|
+
};
|
|
13
|
+
/** Maintained summary for one group. */
|
|
14
|
+
export type AggregateGroup = {
|
|
15
|
+
group: RowKey;
|
|
16
|
+
count: number;
|
|
17
|
+
sum: number;
|
|
18
|
+
/** `sum / count`, or 0 when the group is empty. */
|
|
19
|
+
avg: number;
|
|
20
|
+
min: number | undefined;
|
|
21
|
+
max: number | undefined;
|
|
22
|
+
};
|
|
23
|
+
export type Aggregate<T> = {
|
|
24
|
+
/** Bulk-load the initial rows (replaces current state). */
|
|
25
|
+
hydrate: (rows: Iterable<T>) => void;
|
|
26
|
+
/** Fold one row change into the running aggregates. */
|
|
27
|
+
apply: (change: RowChange<T>) => void;
|
|
28
|
+
/** Current summary for every non-empty group. */
|
|
29
|
+
groups: () => AggregateGroup[];
|
|
30
|
+
/** Current summary for one group, or `undefined` if empty. */
|
|
31
|
+
group: (group: RowKey) => AggregateGroup | undefined;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* An incrementally-maintained aggregation — the DD-lite for `count`/`sum`/`avg`/
|
|
35
|
+
* `min`/`max`, optionally grouped. Feed it the change feed (insert/update/delete)
|
|
36
|
+
* and it updates each group's summary in place: count/sum/avg are O(1); min/max
|
|
37
|
+
* use a value multiset so removing the current extremum recomputes correctly
|
|
38
|
+
* (O(distinct values) only when the extremum leaves).
|
|
39
|
+
*
|
|
40
|
+
* Per-row contributions are tracked by `key`, so updates (including a row moving
|
|
41
|
+
* between groups) and deletes adjust the right group without re-scanning. Use it
|
|
42
|
+
* server-side over the engine's change feed, or client-side over collection
|
|
43
|
+
* diffs.
|
|
44
|
+
*/
|
|
45
|
+
export declare const createAggregate: <T>(options: AggregateOptions<T>) => Aggregate<T>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RowChange } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Horizontal scale — the seam for running the engine across many server
|
|
4
|
+
* instances. The in-process hub only reaches subscribers on the same instance;
|
|
5
|
+
* a {@link ClusterBus} fans every instance's committed changes out to the
|
|
6
|
+
* others, so a mutation on instance A reaches subscribers on instance B.
|
|
7
|
+
*
|
|
8
|
+
* Client-agnostic like the {@link ChangeSource} seam: you supply `publish` +
|
|
9
|
+
* `subscribe` over your bus of choice (Redis stream, Postgres `LISTEN/NOTIFY`,
|
|
10
|
+
* NATS…), and the engine handles fan-out and loop prevention (each message is
|
|
11
|
+
* tagged with the originating instance, and an instance ignores its own).
|
|
12
|
+
*
|
|
13
|
+
* Note: version cursors are per-instance, so a client that reconnects to a
|
|
14
|
+
* *different* instance falls back to a fresh snapshot (correct, just not a
|
|
15
|
+
* catch-up diff) — use sticky sessions if you want cross-instance resume.
|
|
16
|
+
*/
|
|
17
|
+
/** A committed change as it travels over the bus. */
|
|
18
|
+
export type ClusterChange = {
|
|
19
|
+
table: string;
|
|
20
|
+
change: RowChange<unknown>;
|
|
21
|
+
};
|
|
22
|
+
export type ClusterMessage = {
|
|
23
|
+
/** The instance that produced these changes (so peers ignore their own). */
|
|
24
|
+
origin: string;
|
|
25
|
+
changes: ClusterChange[];
|
|
26
|
+
};
|
|
27
|
+
export type ClusterBus = {
|
|
28
|
+
/** Broadcast this instance's committed changes to the others. */
|
|
29
|
+
publish: (message: ClusterMessage) => void | Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Receive other instances' changes; return a function that stops listening.
|
|
32
|
+
* The engine filters out messages from its own `origin`.
|
|
33
|
+
*/
|
|
34
|
+
subscribe: (onMessage: (message: ClusterMessage) => void) => (() => void | Promise<void>) | Promise<() => void | Promise<void>>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* An in-process {@link ClusterBus} — for tests, local dev, or several engines in
|
|
38
|
+
* one process. **Not** cross-process: real horizontal scale needs a bus that
|
|
39
|
+
* spans machines (Redis stream, Postgres `LISTEN/NOTIFY`).
|
|
40
|
+
*/
|
|
41
|
+
export declare const createInMemoryClusterBus: () => ClusterBus;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { RowKey } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* App-provided context for a subscription — typically the authenticated session
|
|
4
|
+
* (user id, roles). Passed to `authorize`, `hydrate`, and `match` so a
|
|
5
|
+
* collection can scope its rows to the caller.
|
|
6
|
+
*/
|
|
7
|
+
export type CollectionContext = Record<string, unknown>;
|
|
8
|
+
export type CollectionDefinition<T, P = void, Ctx = CollectionContext> = {
|
|
9
|
+
/** Collection name — its identity for subscribe (e.g. `orders`). */
|
|
10
|
+
name: string;
|
|
11
|
+
/**
|
|
12
|
+
* Source tables this collection reads. A committed change to any of them
|
|
13
|
+
* updates the collection. Defaults to `[name]`. List several for a join /
|
|
14
|
+
* aggregate collection — which uses the refetch fallback, since a single
|
|
15
|
+
* table's row can't be matched into a multi-table result.
|
|
16
|
+
*/
|
|
17
|
+
tables?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Fetch the initial result set from your database (any ORM). Receives the
|
|
20
|
+
* subscription's params and context so it can filter to the caller.
|
|
21
|
+
*/
|
|
22
|
+
hydrate: (params: P, ctx: Ctx) => Promise<Iterable<T>> | Iterable<T>;
|
|
23
|
+
/** Row identity. Defaults to `row.id`. */
|
|
24
|
+
key?: (row: T) => RowKey;
|
|
25
|
+
/**
|
|
26
|
+
* The query's filter as a JS predicate, for incremental matching. Omit to use
|
|
27
|
+
* the refetch fallback (re-hydrate on every change to the collection).
|
|
28
|
+
*
|
|
29
|
+
* It MUST encode the same row filter as `hydrate`/`authorize`: a change that
|
|
30
|
+
* the predicate accepts is pushed to the subscriber, so a too-loose predicate
|
|
31
|
+
* leaks rows. (Deriving `match` from the same filter as `hydrate` keeps the
|
|
32
|
+
* two in lockstep — the planned adapter convenience.)
|
|
33
|
+
*/
|
|
34
|
+
match?: (row: T, params: P, ctx: Ctx) => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Access control: return `false` (or throw) to deny the subscription. Runs
|
|
37
|
+
* before `hydrate`. Without it a collection is world-readable, so treat it as
|
|
38
|
+
* mandatory for any non-public data.
|
|
39
|
+
*/
|
|
40
|
+
authorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Define a syncable collection. Identity at runtime — it exists for type
|
|
44
|
+
* inference, so `params`/`ctx`/row types flow through `hydrate`/`match`/
|
|
45
|
+
* `authorize` without restating them. Register it with a {@link SyncEngine}.
|
|
46
|
+
*/
|
|
47
|
+
export declare const defineCollection: <T, P = void, Ctx = CollectionContext>(definition: CollectionDefinition<T, P, Ctx>) => CollectionDefinition<T, P, Ctx>;
|
|
48
|
+
/** One input of a join collection. */
|
|
49
|
+
export type JoinSide<Row, P, Ctx> = {
|
|
50
|
+
/** Source table name (the change feed routes its changes to this side). */
|
|
51
|
+
table: string;
|
|
52
|
+
/** Fetch this side's rows for the subscription (scoped to the caller). */
|
|
53
|
+
hydrate: (params: P, ctx: Ctx) => Promise<Iterable<Row>> | Iterable<Row>;
|
|
54
|
+
/** Row identity within this side. */
|
|
55
|
+
key: (row: Row) => RowKey;
|
|
56
|
+
/** Join value — matched for equality against the other side's `on`. */
|
|
57
|
+
on: (row: Row) => RowKey;
|
|
58
|
+
/**
|
|
59
|
+
* Access/predicate filter for incremental changes on this side. A changed row
|
|
60
|
+
* that fails it is treated as a leave (removed from the join), so a row that
|
|
61
|
+
* becomes invisible drops out. Omit only for an unscoped side.
|
|
62
|
+
*/
|
|
63
|
+
match?: (row: Row, params: P, ctx: Ctx) => boolean;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* A collection that is the incremental inner equi-join of two tables. The engine
|
|
67
|
+
* maintains it with an {@link createEquiJoin} operator — a change to either side
|
|
68
|
+
* moves only the affected pairs, instead of re-hydrating the whole join.
|
|
69
|
+
*/
|
|
70
|
+
export type JoinCollectionDefinition<L, R, Out, P = void, Ctx = CollectionContext> = {
|
|
71
|
+
name: string;
|
|
72
|
+
kind: 'join';
|
|
73
|
+
left: JoinSide<L, P, Ctx>;
|
|
74
|
+
right: JoinSide<R, P, Ctx>;
|
|
75
|
+
/** Combine a matched pair into an output row. */
|
|
76
|
+
select: (left: L, right: R) => Out;
|
|
77
|
+
/** Output row identity (must be unique per emitted row). */
|
|
78
|
+
key: (out: Out) => RowKey;
|
|
79
|
+
/** Access control; return false (or throw) to deny the subscription. */
|
|
80
|
+
authorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Define an incremental equi-join collection (see {@link JoinCollectionDefinition}).
|
|
84
|
+
* For a many-to-one join the output can key by the left id; for many-to-many,
|
|
85
|
+
* include both ids in the output and key on the pair.
|
|
86
|
+
*/
|
|
87
|
+
export declare const defineJoinCollection: <L, R, Out, P = void, Ctx = CollectionContext>(definition: Omit<JoinCollectionDefinition<L, R, Out, P, Ctx>, "kind">) => JoinCollectionDefinition<L, R, Out, P, Ctx>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { PresenceHub, PresenceMember } from './presence';
|
|
2
|
+
import type { SyncEngine } from './syncEngine';
|
|
3
|
+
/**
|
|
4
|
+
* Wire protocol for the sync-engine WebSocket. One connection multiplexes many
|
|
5
|
+
* collection subscriptions, each tagged with a client-chosen `id`.
|
|
6
|
+
*/
|
|
7
|
+
/** Client → server. */
|
|
8
|
+
export type ClientFrame = {
|
|
9
|
+
type: 'subscribe';
|
|
10
|
+
id: string;
|
|
11
|
+
collection: string;
|
|
12
|
+
params?: unknown;
|
|
13
|
+
/** Resume from a version already applied (catch-up instead of snapshot). */
|
|
14
|
+
since?: number;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'unsubscribe';
|
|
17
|
+
id: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'mutate';
|
|
20
|
+
mutationId: number;
|
|
21
|
+
name: string;
|
|
22
|
+
args?: unknown;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'presence-join';
|
|
25
|
+
room: string;
|
|
26
|
+
memberId: string;
|
|
27
|
+
state: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'presence-set';
|
|
30
|
+
room: string;
|
|
31
|
+
state: unknown;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'presence-leave';
|
|
34
|
+
room: string;
|
|
35
|
+
};
|
|
36
|
+
/** One subscription's delta within a {@link ServerFrame} `frame`. */
|
|
37
|
+
export type FrameDiff<T = unknown> = {
|
|
38
|
+
id: string;
|
|
39
|
+
added: T[];
|
|
40
|
+
removed: T[];
|
|
41
|
+
changed: T[];
|
|
42
|
+
};
|
|
43
|
+
/** Server → client. `version` is the change-feed watermark this frame brings. */
|
|
44
|
+
export type ServerFrame<T = unknown> = {
|
|
45
|
+
type: 'snapshot';
|
|
46
|
+
id: string;
|
|
47
|
+
rows: T[];
|
|
48
|
+
version?: number;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'diff';
|
|
51
|
+
id: string;
|
|
52
|
+
added: T[];
|
|
53
|
+
removed: T[];
|
|
54
|
+
changed: T[];
|
|
55
|
+
version?: number;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'frame';
|
|
58
|
+
version?: number;
|
|
59
|
+
diffs: FrameDiff<T>[];
|
|
60
|
+
} | {
|
|
61
|
+
type: 'presence';
|
|
62
|
+
room: string;
|
|
63
|
+
joined: PresenceMember<T>[];
|
|
64
|
+
updated: PresenceMember<T>[];
|
|
65
|
+
left: string[];
|
|
66
|
+
} | {
|
|
67
|
+
type: 'error';
|
|
68
|
+
id?: string;
|
|
69
|
+
message: string;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'ack';
|
|
72
|
+
mutationId: number;
|
|
73
|
+
result?: unknown;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'reject';
|
|
76
|
+
mutationId: number;
|
|
77
|
+
message: string;
|
|
78
|
+
};
|
|
79
|
+
export type SyncConnectionOptions = {
|
|
80
|
+
engine: SyncEngine;
|
|
81
|
+
/** Resolved auth context for this connection; passed to every subscribe. */
|
|
82
|
+
ctx: unknown;
|
|
83
|
+
/** Send a frame to the client (the transport serializes it). */
|
|
84
|
+
send: (frame: ServerFrame) => void;
|
|
85
|
+
/** Optional presence hub; enables the `presence-*` frames (see createPresenceHub). */
|
|
86
|
+
presence?: PresenceHub;
|
|
87
|
+
};
|
|
88
|
+
export type SyncConnection = {
|
|
89
|
+
/** Handle one client frame (a parsed object or a raw JSON string). */
|
|
90
|
+
handle: (raw: unknown) => Promise<void>;
|
|
91
|
+
/** Tear down every subscription on this connection (call on socket close). */
|
|
92
|
+
close: () => void;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The per-connection protocol handler — transport-agnostic glue between a single
|
|
96
|
+
* client socket and the {@link SyncEngine}. It owns that connection's
|
|
97
|
+
* subscriptions: a `subscribe` frame authorizes + hydrates and replies with a
|
|
98
|
+
* `snapshot`, then streams `diff` frames; `unsubscribe`/`close` release views.
|
|
99
|
+
*
|
|
100
|
+
* Pure (no WebSocket import) so it can be unit-tested with a fake `send`; the
|
|
101
|
+
* Elysia `syncSocket` plugin is the thin adapter that feeds it socket events.
|
|
102
|
+
*/
|
|
103
|
+
export declare const createSyncConnection: ({ engine, ctx, send, presence }: SyncConnectionOptions) => SyncConnection;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { AggregateGroup } from './aggregate';
|
|
2
|
+
import type { RowChange, RowKey, ViewDiff } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Composable incremental dataflow (the general operator graph, in progress).
|
|
5
|
+
*
|
|
6
|
+
* Every edge in the graph is a stream of keyed **changes** — an `upsert` (insert
|
|
7
|
+
* or update, last-write-wins per key) or a `delete`. Collapsing added/changed
|
|
8
|
+
* into one `upsert` is what makes operators compose: `filter`/`map` become
|
|
9
|
+
* stateless stream transforms, `join` reuses the equi-join operator, and a
|
|
10
|
+
* `materialize` sink turns the final stream back into a `{ added, removed,
|
|
11
|
+
* changed }` diff for the transport.
|
|
12
|
+
*/
|
|
13
|
+
export type Change<T> = {
|
|
14
|
+
op: 'upsert' | 'delete';
|
|
15
|
+
key: RowKey;
|
|
16
|
+
row: T;
|
|
17
|
+
};
|
|
18
|
+
/** A single-input incremental operator: a batch of input changes → output changes. */
|
|
19
|
+
export type Operator<In, Out> = {
|
|
20
|
+
push: (changes: Change<In>[]) => Change<Out>[];
|
|
21
|
+
};
|
|
22
|
+
/** Lift a table row change into a dataflow change (a graph source). */
|
|
23
|
+
export declare const fromRowChange: <T>(change: RowChange<T>, key: (row: T) => RowKey) => Change<T>;
|
|
24
|
+
/**
|
|
25
|
+
* Keep only rows matching `predicate`. Stateless: a row that fails the predicate
|
|
26
|
+
* is emitted as a `delete` (downstream removes it if present, else no-op), so a
|
|
27
|
+
* row that stops matching leaves correctly.
|
|
28
|
+
*/
|
|
29
|
+
export declare const filterOp: <T>(predicate: (row: T) => boolean) => Operator<T, T>;
|
|
30
|
+
/**
|
|
31
|
+
* Transform each row. Stateless; preserves identity unless `rekey` is given (to
|
|
32
|
+
* derive the output key from the mapped row).
|
|
33
|
+
*/
|
|
34
|
+
export declare const mapOp: <In, Out>(transform: (row: In) => Out, rekey?: (row: Out) => RowKey) => Operator<In, Out>;
|
|
35
|
+
/** Compose two operators into one (`a` then `b`). Nest for longer chains. */
|
|
36
|
+
export declare const chain: <A, B, C>(a: Operator<A, B>, b: Operator<B, C>) => Operator<A, C>;
|
|
37
|
+
export type AggregateOpOptions<In> = {
|
|
38
|
+
/** Input row identity (to track each row's contribution across updates). */
|
|
39
|
+
key: (row: In) => RowKey;
|
|
40
|
+
/** Group rows by this key (omit for one `''` group). */
|
|
41
|
+
groupBy?: (row: In) => RowKey;
|
|
42
|
+
/** Numeric value for sum/avg/min/max (omit for a count-only aggregate). */
|
|
43
|
+
value?: (row: In) => number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Aggregate a change stream into a stream of group summaries — `count`/`sum`/
|
|
47
|
+
* `avg`/`min`/`max` per group, maintained incrementally (wraps
|
|
48
|
+
* {@link createAggregate}). Emits an `upsert` of the group summary for each group
|
|
49
|
+
* a batch touched, or a `delete` when a group empties. Output is keyed by group,
|
|
50
|
+
* so it composes downstream like any other operator (e.g. after a join).
|
|
51
|
+
*/
|
|
52
|
+
export declare const aggregateOp: <In>(options: AggregateOpOptions<In>) => Operator<In, AggregateGroup>;
|
|
53
|
+
export type OrderByOptions<T> = {
|
|
54
|
+
/** Row identity. */
|
|
55
|
+
key: (row: T) => RowKey;
|
|
56
|
+
/** Sort comparator (ascending: negative = a before b). */
|
|
57
|
+
compare: (a: T, b: T) => number;
|
|
58
|
+
/** Keep at most this many rows (the top-N window). */
|
|
59
|
+
limit?: number;
|
|
60
|
+
/** Skip this many rows from the front (pagination). Defaults to 0. */
|
|
61
|
+
offset?: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Maintain a sorted top-N window: keep only the `[offset, offset + limit)` slice
|
|
65
|
+
* by `compare`, emitting which rows entered/left the window as input changes.
|
|
66
|
+
* A single insert can both add a row and evict the one it displaced — both are
|
|
67
|
+
* emitted. Bounded output (≤ limit upserts per batch); it re-sorts its input, so
|
|
68
|
+
* place it where the input is already narrowed (e.g. after a filter/join).
|
|
69
|
+
*
|
|
70
|
+
* The window is the right *set* of rows; sort them by the same comparator on the
|
|
71
|
+
* client for display order (cheap for N rows — the diff protocol is unordered).
|
|
72
|
+
*/
|
|
73
|
+
export declare const orderByOp: <T>(options: OrderByOptions<T>) => Operator<T, T>;
|
|
74
|
+
/** A two-input incremental equi-join node. */
|
|
75
|
+
export type JoinNode<L, R, Out> = {
|
|
76
|
+
hydrate: (left: Iterable<L>, right: Iterable<R>) => void;
|
|
77
|
+
pushLeft: (changes: Change<L>[]) => Change<Out>[];
|
|
78
|
+
pushRight: (changes: Change<R>[]) => Change<Out>[];
|
|
79
|
+
rows: () => Out[];
|
|
80
|
+
};
|
|
81
|
+
export type JoinNodeOptions<L, R, Out> = {
|
|
82
|
+
leftKey: (left: L) => RowKey;
|
|
83
|
+
rightKey: (right: R) => RowKey;
|
|
84
|
+
leftOn: (left: L) => RowKey;
|
|
85
|
+
rightOn: (right: R) => RowKey;
|
|
86
|
+
select: (left: L, right: R) => Out;
|
|
87
|
+
/** Provide for a LEFT join: output for a left row with no match. */
|
|
88
|
+
selectUnmatched?: (left: L) => Out;
|
|
89
|
+
/** Output row identity (unique per emitted pair). */
|
|
90
|
+
key: (out: Out) => RowKey;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* A join as a dataflow node — reuses {@link createEquiJoin} and converts its
|
|
94
|
+
* `{ added, removed, changed }` deltas into the upsert/delete change stream.
|
|
95
|
+
*/
|
|
96
|
+
export declare const joinNode: <L, R, Out>(options: JoinNodeOptions<L, R, Out>) => JoinNode<L, R, Out>;
|
|
97
|
+
export type Materializer<T> = {
|
|
98
|
+
/** Replace the set with initial rows (no diff emitted). */
|
|
99
|
+
hydrate: (rows: Iterable<T>) => void;
|
|
100
|
+
/** Apply a change stream and return the resulting result-set diff. */
|
|
101
|
+
apply: (changes: Change<T>[]) => ViewDiff<T>;
|
|
102
|
+
rows: () => T[];
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The graph sink: maintain a keyed result set from a change stream and emit the
|
|
106
|
+
* `{ added, removed, changed }` diff each batch produces — the boundary back to
|
|
107
|
+
* the transport / client.
|
|
108
|
+
*/
|
|
109
|
+
export declare const materialize: <T>(key: (row: T) => RowKey, equals?: (a: T, b: T) => boolean) => Materializer<T>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RowChange, RowKey, ViewDiff } from './types';
|
|
2
|
+
export type EquiJoinOptions<L, R, Out> = {
|
|
3
|
+
/** Left row identity. */
|
|
4
|
+
leftKey: (left: L) => RowKey;
|
|
5
|
+
/** Right row identity. */
|
|
6
|
+
rightKey: (right: R) => RowKey;
|
|
7
|
+
/** Join value on the left (matched against `rightOn`). */
|
|
8
|
+
leftOn: (left: L) => RowKey;
|
|
9
|
+
/** Join value on the right (matched against `leftOn`). */
|
|
10
|
+
rightOn: (right: R) => RowKey;
|
|
11
|
+
/** Combine a matched pair into an output row. */
|
|
12
|
+
select: (left: L, right: R) => Out;
|
|
13
|
+
/**
|
|
14
|
+
* Provide to make this a LEFT join: a left row with no matching right emits
|
|
15
|
+
* `selectUnmatched(left)` instead of nothing, and is replaced by matched rows
|
|
16
|
+
* once a right appears (and reverts when the last right leaves). Omit for an
|
|
17
|
+
* inner join.
|
|
18
|
+
*/
|
|
19
|
+
selectUnmatched?: (left: L) => Out;
|
|
20
|
+
/**
|
|
21
|
+
* Equality used to detect when a matched pair's output value changed.
|
|
22
|
+
* Defaults to a shallow compare of own enumerable properties.
|
|
23
|
+
*/
|
|
24
|
+
equals?: (a: Out, b: Out) => boolean;
|
|
25
|
+
};
|
|
26
|
+
export type EquiJoin<L, R, Out> = {
|
|
27
|
+
/** Bulk-load both inputs (replaces current state). */
|
|
28
|
+
hydrate: (left: Iterable<L>, right: Iterable<R>) => void;
|
|
29
|
+
/** Apply a change to the left input; returns the output diff. */
|
|
30
|
+
applyLeft: (change: RowChange<L>) => ViewDiff<Out>;
|
|
31
|
+
/** Apply a change to the right input; returns the output diff. */
|
|
32
|
+
applyRight: (change: RowChange<R>) => ViewDiff<Out>;
|
|
33
|
+
/** Current joined rows. */
|
|
34
|
+
rows: () => Out[];
|
|
35
|
+
/** Number of joined rows. */
|
|
36
|
+
size: () => number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* An incrementally-maintained equi-join (the differential-dataflow core, bounded
|
|
40
|
+
* to a single two-input equality join). Hydrate both sides, then feed each side's
|
|
41
|
+
* row changes through {@link EquiJoin.applyLeft} / `applyRight`: it indexes both
|
|
42
|
+
* inputs by the join value and emits only the `{ added, removed, changed }`
|
|
43
|
+
* joined rows the change affects — touching the matching pairs, not the whole
|
|
44
|
+
* result. Inner by default; pass `selectUnmatched` for a LEFT join.
|
|
45
|
+
*
|
|
46
|
+
* Output rows are keyed internally by the `(leftKey, rightKey)` pair, so it
|
|
47
|
+
* handles many-to-many. The consumer keys the emitted rows by its own key; for a
|
|
48
|
+
* many-to-many join, include both ids in the output so that key is unique
|
|
49
|
+
* (a many-to-one join can key by the left id alone).
|
|
50
|
+
*/
|
|
51
|
+
export declare const createEquiJoin: <L, R, Out>(options: EquiJoinOptions<L, R, Out>) => EquiJoin<L, R, Out>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AggregateGroup } from './aggregate';
|
|
2
|
+
import type { CollectionContext } from './collection';
|
|
3
|
+
import type { RowChange, RowKey, ViewDiff } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Declarative incremental queries — the front door to the operator graph. Build a
|
|
6
|
+
* pipeline with {@link query} (`source → filter → map → join → groupBy`); the
|
|
7
|
+
* engine instantiates it per subscription, hydrates each source, and routes each
|
|
8
|
+
* table's changes through the wired operators, emitting result diffs.
|
|
9
|
+
*/
|
|
10
|
+
/** A table this query reads. */
|
|
11
|
+
export type GraphSource<Row, P = void, Ctx = CollectionContext> = {
|
|
12
|
+
table: string;
|
|
13
|
+
hydrate: (params: P, ctx: Ctx) => Promise<Iterable<Row>> | Iterable<Row>;
|
|
14
|
+
key: (row: Row) => RowKey;
|
|
15
|
+
/** Scope incremental changes (a row that fails it leaves). */
|
|
16
|
+
match?: (row: Row, params: P, ctx: Ctx) => boolean;
|
|
17
|
+
};
|
|
18
|
+
export type JoinOptions<Left, Right, Out> = {
|
|
19
|
+
/** Join value on the left (current) stream. */
|
|
20
|
+
on: (left: Left) => RowKey;
|
|
21
|
+
/** Join value on the right source. */
|
|
22
|
+
rightOn: (right: Right) => RowKey;
|
|
23
|
+
/** Combine a matched pair. */
|
|
24
|
+
select: (left: Left, right: Right) => Out;
|
|
25
|
+
/**
|
|
26
|
+
* Provide to make this a LEFT join: output for a left row with no matching
|
|
27
|
+
* right (e.g. a user with zero orders). Omit for an inner join.
|
|
28
|
+
*/
|
|
29
|
+
selectUnmatched?: (left: Left) => Out;
|
|
30
|
+
/** Output row identity (unique per pair). */
|
|
31
|
+
key: (out: Out) => RowKey;
|
|
32
|
+
};
|
|
33
|
+
export type GroupByOptions<Row> = {
|
|
34
|
+
/** Input row identity (to track contributions). */
|
|
35
|
+
key: (row: Row) => RowKey;
|
|
36
|
+
groupBy?: (row: Row) => RowKey;
|
|
37
|
+
value?: (row: Row) => number;
|
|
38
|
+
};
|
|
39
|
+
export type OrderByQueryOptions<Row> = {
|
|
40
|
+
/** Row identity. */
|
|
41
|
+
key: (row: Row) => RowKey;
|
|
42
|
+
/** Sort comparator (ascending). */
|
|
43
|
+
compare: (a: Row, b: Row) => number;
|
|
44
|
+
/** Keep at most this many rows (top-N). */
|
|
45
|
+
limit?: number;
|
|
46
|
+
/** Skip this many from the front (pagination). */
|
|
47
|
+
offset?: number;
|
|
48
|
+
};
|
|
49
|
+
/** A live, instantiated graph for one subscription. */
|
|
50
|
+
export type GraphInstance<Out> = {
|
|
51
|
+
tables: string[];
|
|
52
|
+
hydrate: () => Promise<Out[]>;
|
|
53
|
+
applyChange: (table: string, change: RowChange<unknown>) => ViewDiff<Out>;
|
|
54
|
+
};
|
|
55
|
+
export type Query<Row, P = void, Ctx = CollectionContext> = {
|
|
56
|
+
filter: (predicate: (row: Row, params: P, ctx: Ctx) => boolean) => Query<Row, P, Ctx>;
|
|
57
|
+
map: <Out>(transform: (row: Row) => Out, rekey?: (row: Out) => RowKey) => Query<Out, P, Ctx>;
|
|
58
|
+
join: <Right, Out>(right: GraphSource<Right, P, Ctx> | Query<Right, P, Ctx>, options: JoinOptions<Row, Right, Out>) => Query<Out, P, Ctx>;
|
|
59
|
+
/**
|
|
60
|
+
* LEFT join: like {@link join} but keeps left rows with no match, emitting
|
|
61
|
+
* `selectUnmatched(left)` for them (required, so the intent is explicit).
|
|
62
|
+
*/
|
|
63
|
+
leftJoin: <Right, Out>(right: GraphSource<Right, P, Ctx> | Query<Right, P, Ctx>, options: JoinOptions<Row, Right, Out> & {
|
|
64
|
+
selectUnmatched: (left: Row) => Out;
|
|
65
|
+
}) => Query<Out, P, Ctx>;
|
|
66
|
+
groupBy: (options: GroupByOptions<Row>) => Query<AggregateGroup, P, Ctx>;
|
|
67
|
+
orderBy: (options: OrderByQueryOptions<Row>) => Query<Row, P, Ctx>;
|
|
68
|
+
/** Source tables this query reads. */
|
|
69
|
+
tables: () => string[];
|
|
70
|
+
/** Instantiate the graph for one subscription's params/ctx. */
|
|
71
|
+
instantiate: (params: P, ctx: Ctx) => GraphInstance<Row>;
|
|
72
|
+
};
|
|
73
|
+
/** Start a query from a source table. */
|
|
74
|
+
export declare const query: <Row, P = void, Ctx = CollectionContext>(source: GraphSource<Row, P, Ctx>) => Query<Row, P, Ctx>;
|
|
75
|
+
/** A collection backed by an incremental operator graph (see {@link query}). */
|
|
76
|
+
export type GraphCollectionDefinition<Out, P = void, Ctx = CollectionContext> = {
|
|
77
|
+
name: string;
|
|
78
|
+
kind: 'graph';
|
|
79
|
+
query: Query<Out, P, Ctx>;
|
|
80
|
+
authorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;
|
|
81
|
+
/** Output row identity (used by the engine/transport to key result rows). */
|
|
82
|
+
key: (out: Out) => RowKey;
|
|
83
|
+
};
|
|
84
|
+
/** Define a collection whose result is maintained by an operator graph. */
|
|
85
|
+
export declare const defineGraphCollection: <Out, P = void, Ctx = CollectionContext>(definition: Omit<GraphCollectionDefinition<Out, P, Ctx>, "kind">) => GraphCollectionDefinition<Out, P, Ctx>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @absolutejs/sync engine (Tier 3 — sync engine MVP, server-side).
|
|
3
|
+
*
|
|
4
|
+
* Row-level reactive query results on your own database. Hydrate a query's
|
|
5
|
+
* result set once, then maintain it incrementally as rows change and push the
|
|
6
|
+
* diffs to subscribers — instead of refetching the whole query.
|
|
7
|
+
*
|
|
8
|
+
* This entry currently exposes the predicate-matching IVM core
|
|
9
|
+
* ({@link createMaterializedView}); the collection registry, view syncer, diff
|
|
10
|
+
* transport, and client store land here as the read and write paths fill in.
|
|
11
|
+
*/
|
|
12
|
+
export { createMaterializedView, isEmptyViewDiff } from './materializedView';
|
|
13
|
+
export type { MaterializedView, MaterializedViewOptions } from './materializedView';
|
|
14
|
+
export { createAggregate } from './aggregate';
|
|
15
|
+
export type { Aggregate, AggregateGroup, AggregateOptions } from './aggregate';
|
|
16
|
+
export { createEquiJoin } from './equiJoin';
|
|
17
|
+
export type { EquiJoin, EquiJoinOptions } from './equiJoin';
|
|
18
|
+
export { aggregateOp, chain, filterOp, fromRowChange, joinNode, mapOp, materialize, orderByOp } from './dataflow';
|
|
19
|
+
export type { AggregateOpOptions, Change, JoinNode, JoinNodeOptions, Materializer, Operator, OrderByOptions } from './dataflow';
|
|
20
|
+
export type { ChangeSource, EmitChange, ParsedChange, RowChange, RowKey, RowOp, ViewDiff } from './types';
|
|
21
|
+
export { createPollingChangeSource, parseOutboxRow } from './pollingSource';
|
|
22
|
+
export type { OutboxRow, PollingChangeSourceOptions } from './pollingSource';
|
|
23
|
+
export { createInMemoryClusterBus } from './cluster';
|
|
24
|
+
export type { ClusterBus, ClusterChange, ClusterMessage } from './cluster';
|
|
25
|
+
export { createPresenceHub } from './presence';
|
|
26
|
+
export type { PresenceDiff, PresenceHandle, PresenceHub, PresenceMember } from './presence';
|
|
27
|
+
export { defineCollection, defineJoinCollection } from './collection';
|
|
28
|
+
export type { CollectionContext, CollectionDefinition, JoinCollectionDefinition, JoinSide } from './collection';
|
|
29
|
+
export { defineReactiveQuery } from './reactive';
|
|
30
|
+
export type { ReactiveQueryContext, ReactiveQueryDefinition, ReadHandle, TableReader } from './reactive';
|
|
31
|
+
export { defineGraphCollection, query } from './graph';
|
|
32
|
+
export type { GraphCollectionDefinition, GraphInstance, GraphSource, GroupByOptions, JoinOptions, OrderByQueryOptions, Query } from './graph';
|
|
33
|
+
export { defineMutation } from './mutation';
|
|
34
|
+
export type { MutationActions, MutationDefinition, MutationHandler, TableWriter, TransactionRunner } from './mutation';
|
|
35
|
+
export { createSyncEngine, UnauthorizedError } from './syncEngine';
|
|
36
|
+
export type { SubscribeArgs, Subscription, SyncEngine } from './syncEngine';
|
|
37
|
+
export { hydrateRoute, mutateRoute } from './routes';
|
|
38
|
+
export type { SyncRouteContext } from './routes';
|
|
39
|
+
export { createSyncConnection } from './connection';
|
|
40
|
+
export type { ClientFrame, FrameDiff, ServerFrame, SyncConnection, SyncConnectionOptions } from './connection';
|