@absolutejs/sync 0.3.0 → 0.5.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 +71 -17
- package/dist/engine/index.d.ts +8 -0
- package/dist/engine/index.js +323 -11
- package/dist/engine/index.js.map +8 -4
- package/dist/engine/permissions.d.ts +51 -0
- package/dist/engine/search.d.ts +61 -0
- package/dist/engine/syncEngine.d.ts +24 -0
- package/dist/engine/textIndex.d.ts +33 -0
- package/dist/engine/vectorIndex.d.ts +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CollectionContext } from './collection';
|
|
2
|
+
/**
|
|
3
|
+
* Declarative, row-level access control keyed by table — the engine enforces it,
|
|
4
|
+
* so a rule lives in one place instead of being restated across a collection's
|
|
5
|
+
* `authorize` (gate), `hydrate` (DB filter), and `match` (incremental filter).
|
|
6
|
+
* This is the BYO-database analogue of Convex/Zero permissions: plain predicate
|
|
7
|
+
* functions over `(ctx, row)`, applied uniformly to reads and writes.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* A row-level read rule: may `ctx` see `row`? Return `true` to allow. The engine
|
|
11
|
+
* applies it to every row it would emit for the table — the initial snapshot, an
|
|
12
|
+
* incremental diff, a catch-up diff, and the one-shot hydrate — and to the reads
|
|
13
|
+
* a reactive query makes through `ctx.db`. So a row a caller can't see never
|
|
14
|
+
* reaches them, even if the collection's `hydrate`/`match` are too loose.
|
|
15
|
+
*/
|
|
16
|
+
export type ReadRule<Row = unknown, Ctx = CollectionContext> = (ctx: Ctx, row: Row) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* A row-level write rule: may `ctx` perform this write? Return `true` to allow; a
|
|
19
|
+
* `false` rejects the mutation with `UnauthorizedError`, rolling back its
|
|
20
|
+
* transaction. For `insert` the rule sees the row being created. For
|
|
21
|
+
* `update`/`delete` it sees the *existing* row when the table has a reader with
|
|
22
|
+
* `get` (so the check can't be spoofed by the client payload), otherwise the row
|
|
23
|
+
* passed to the action.
|
|
24
|
+
*/
|
|
25
|
+
export type WriteRule<Row = unknown, Ctx = CollectionContext> = (ctx: Ctx, row: Row) => boolean;
|
|
26
|
+
/** Declarative permissions for one table's rows. An omitted rule allows. */
|
|
27
|
+
export type TablePermissions<Row = unknown, Ctx = CollectionContext> = {
|
|
28
|
+
/** Who may read a row — filters every row the engine emits for this table. */
|
|
29
|
+
read?: ReadRule<Row, Ctx>;
|
|
30
|
+
/** Who may insert a row. Falls back to {@link TablePermissions.write}. */
|
|
31
|
+
insert?: WriteRule<Row, Ctx>;
|
|
32
|
+
/** Who may update a row. Falls back to {@link TablePermissions.write}. */
|
|
33
|
+
update?: WriteRule<Row, Ctx>;
|
|
34
|
+
/** Who may delete a row. Falls back to {@link TablePermissions.write}. */
|
|
35
|
+
delete?: WriteRule<Row, Ctx>;
|
|
36
|
+
/** Default write rule for any of insert/update/delete without a specific one. */
|
|
37
|
+
write?: WriteRule<Row, Ctx>;
|
|
38
|
+
};
|
|
39
|
+
/** A `table` → {@link TablePermissions} map. */
|
|
40
|
+
export type PermissionsDefinition<Ctx = CollectionContext> = Record<string, TablePermissions<any, Ctx>>;
|
|
41
|
+
/**
|
|
42
|
+
* Define declarative, row-level permissions keyed by table. Identity at runtime —
|
|
43
|
+
* it exists for type inference. Pass to `createSyncEngine({ permissions })` or
|
|
44
|
+
* register incrementally with `engine.registerPermissions(table, rules)`.
|
|
45
|
+
*
|
|
46
|
+
* Scope: read rules are enforced for single-table `view` collections and for the
|
|
47
|
+
* reads a reactive query makes through `ctx.db`; join/graph collections scope via
|
|
48
|
+
* their own `hydrate`/`match`. Write rules are enforced in
|
|
49
|
+
* `actions.insert/update/delete` (not the low-level `actions.change`).
|
|
50
|
+
*/
|
|
51
|
+
export declare const definePermissions: <Ctx = CollectionContext>(permissions: PermissionsDefinition<Ctx>) => PermissionsDefinition<Ctx>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CollectionContext } from './collection';
|
|
2
|
+
import type { RowKey } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* A scored search result: the matched row and its relevance score (higher is
|
|
5
|
+
* more relevant). A search collection sorts hits descending and tags each
|
|
6
|
+
* emitted row with its score (see {@link SEARCH_SCORE_FIELD}).
|
|
7
|
+
*/
|
|
8
|
+
export type SearchHit<T> = {
|
|
9
|
+
row: T;
|
|
10
|
+
score: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* An incremental search index over a row set, queried by `Q` (a string for
|
|
14
|
+
* full-text, a vector for similarity). Maintained as rows are added/removed, so
|
|
15
|
+
* the collection that owns it stays live as the corpus changes.
|
|
16
|
+
* {@link createTextIndex} and {@link createVectorIndex} implement it.
|
|
17
|
+
*/
|
|
18
|
+
export type SearchIndex<T, Q> = {
|
|
19
|
+
/** Add or replace a row (upsert by key). */
|
|
20
|
+
add: (row: T) => void;
|
|
21
|
+
/** Remove a row by key. */
|
|
22
|
+
remove: (key: RowKey) => void;
|
|
23
|
+
/** Top-`limit` hits for `query`, sorted by descending score. */
|
|
24
|
+
search: (query: Q, limit: number) => SearchHit<T>[];
|
|
25
|
+
/** Number of indexed rows. */
|
|
26
|
+
size: () => number;
|
|
27
|
+
/** Drop every indexed row. */
|
|
28
|
+
clear: () => void;
|
|
29
|
+
};
|
|
30
|
+
/** The field a search collection adds to each emitted row carrying its score. */
|
|
31
|
+
export declare const SEARCH_SCORE_FIELD = "_score";
|
|
32
|
+
export type SearchCollectionDefinition<T, Query = string, Ctx = CollectionContext> = {
|
|
33
|
+
/** Collection name — its identity for subscribe. */
|
|
34
|
+
name: string;
|
|
35
|
+
kind: 'search';
|
|
36
|
+
/** Source table whose committed changes keep the index live. */
|
|
37
|
+
table: string;
|
|
38
|
+
/** Build the (empty) index — e.g. `() => createTextIndex({ ... })`. */
|
|
39
|
+
index: () => SearchIndex<T, Query>;
|
|
40
|
+
/** The full corpus to index on first subscribe (e.g. a DB read). */
|
|
41
|
+
source: () => Promise<Iterable<T>> | Iterable<T>;
|
|
42
|
+
/** Row identity. */
|
|
43
|
+
key: (row: T) => RowKey;
|
|
44
|
+
/** Max results returned. Defaults to 20. */
|
|
45
|
+
limit?: number;
|
|
46
|
+
/** Access control: return `false` (or throw) to deny the subscription. */
|
|
47
|
+
authorize?: (query: Query, ctx: Ctx) => boolean | Promise<boolean>;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Define a live search collection: an index (full-text via {@link createTextIndex}
|
|
51
|
+
* or vector via {@link createVectorIndex}) maintained from a source table's
|
|
52
|
+
* change feed. The subscription's `params` *are* the query — a string for
|
|
53
|
+
* full-text, a vector for similarity. Register it with
|
|
54
|
+
* {@link SyncEngine.registerSearch}; the client receives the ranked top-K as a
|
|
55
|
+
* normal collection, re-ranked live as rows change. Each emitted row carries its
|
|
56
|
+
* relevance under {@link SEARCH_SCORE_FIELD}, so the client can sort by it.
|
|
57
|
+
*
|
|
58
|
+
* The corpus is the whole table; a row-level read permission on the table (see
|
|
59
|
+
* {@link definePermissions}) still filters a caller's hits.
|
|
60
|
+
*/
|
|
61
|
+
export declare const defineSearchCollection: <T, Query = string, Ctx = CollectionContext>(definition: Omit<SearchCollectionDefinition<T, Query, Ctx>, "kind">) => SearchCollectionDefinition<T, Query, Ctx>;
|
|
@@ -2,6 +2,8 @@ import type { CollectionContext, CollectionDefinition, JoinCollectionDefinition
|
|
|
2
2
|
import type { GraphCollectionDefinition } from './graph';
|
|
3
3
|
import type { MutationDefinition, TableWriter, TransactionRunner } from './mutation';
|
|
4
4
|
import type { ReactiveQueryDefinition, TableReader } from './reactive';
|
|
5
|
+
import type { PermissionsDefinition, TablePermissions } from './permissions';
|
|
6
|
+
import type { SearchCollectionDefinition } from './search';
|
|
5
7
|
import type { ClusterBus } from './cluster';
|
|
6
8
|
import type { ChangeSource, RowChange, ViewDiff } from './types';
|
|
7
9
|
/**
|
|
@@ -45,6 +47,12 @@ export type SyncEngine = {
|
|
|
45
47
|
registerJoin: <L, R, Out, P = void, Ctx = CollectionContext>(collection: JoinCollectionDefinition<L, R, Out, P, Ctx>) => void;
|
|
46
48
|
/** Register an operator-graph collection (see {@link defineGraphCollection}). */
|
|
47
49
|
registerGraph: <Out, P = void, Ctx = CollectionContext>(collection: GraphCollectionDefinition<Out, P, Ctx>) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Register a live search collection (see {@link defineSearchCollection}): a
|
|
52
|
+
* full-text or vector index maintained from a source table's change feed and
|
|
53
|
+
* queried by the subscription's params, returning the ranked top-K live.
|
|
54
|
+
*/
|
|
55
|
+
registerSearch: <T, Query = string, Ctx = CollectionContext>(collection: SearchCollectionDefinition<T, Query, Ctx>) => void;
|
|
48
56
|
/**
|
|
49
57
|
* Open a live subscription: authorize, hydrate the initial set, and stream
|
|
50
58
|
* diffs as changes arrive. Rejects with {@link UnauthorizedError} on deny.
|
|
@@ -96,6 +104,13 @@ export type SyncEngine = {
|
|
|
96
104
|
* ORM). Required for every table a reactive query reads.
|
|
97
105
|
*/
|
|
98
106
|
registerReader: <Ctx = CollectionContext>(table: string, reader: TableReader<Ctx>) => void;
|
|
107
|
+
/**
|
|
108
|
+
* Register declarative, row-level permissions for a `table` (see
|
|
109
|
+
* {@link definePermissions}). Read rules filter every row the engine emits for
|
|
110
|
+
* the table; write rules gate `actions.insert/update/delete`. Equivalent to a
|
|
111
|
+
* `permissions` entry on {@link createSyncEngine}.
|
|
112
|
+
*/
|
|
113
|
+
registerPermissions: <Row = unknown, Ctx = CollectionContext>(table: string, rules: TablePermissions<Row, Ctx>) => void;
|
|
99
114
|
/**
|
|
100
115
|
* Run a registered mutation: authorize, invoke its handler (which writes and
|
|
101
116
|
* emits changes via `applyChange`), and resolve with the handler's result.
|
|
@@ -118,6 +133,15 @@ export type SyncEngineOptions = {
|
|
|
118
133
|
* mutations without a transaction (each writer call is its own DB op).
|
|
119
134
|
*/
|
|
120
135
|
transaction?: TransactionRunner;
|
|
136
|
+
/**
|
|
137
|
+
* Declarative, row-level permissions keyed by table (see
|
|
138
|
+
* {@link definePermissions}). Read rules filter every row the engine emits;
|
|
139
|
+
* write rules gate `actions.insert/update/delete`. Add more later with
|
|
140
|
+
* {@link SyncEngine.registerPermissions}. Type the rules at the
|
|
141
|
+
* `definePermissions<YourCtx>(...)` call site; the engine accepts any context
|
|
142
|
+
* (it threads `ctx` untyped to your rules).
|
|
143
|
+
*/
|
|
144
|
+
permissions?: PermissionsDefinition<any>;
|
|
121
145
|
};
|
|
122
146
|
/**
|
|
123
147
|
* The Tier 3 sync engine: a registry of collections plus the view syncer. It is
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RowKey } from './types';
|
|
2
|
+
import type { SearchIndex } from './search';
|
|
3
|
+
/**
|
|
4
|
+
* An incremental full-text index with BM25 ranking — the keyword-search half of
|
|
5
|
+
* the search surface (see {@link createVectorIndex} for semantic search). Pure
|
|
6
|
+
* and dependency-free: an in-memory inverted index maintained as rows are
|
|
7
|
+
* added/removed, so a {@link defineSearchCollection} stays live as the corpus
|
|
8
|
+
* changes. For a large corpus back it with your DB's FTS instead; this is the
|
|
9
|
+
* BYO, no-extension default.
|
|
10
|
+
*/
|
|
11
|
+
export type TextIndexOptions<T> = {
|
|
12
|
+
/** Row identity. */
|
|
13
|
+
key: (row: T) => RowKey;
|
|
14
|
+
/** Fields whose text is indexed. Their values are stringified and joined. */
|
|
15
|
+
fields: (keyof T)[];
|
|
16
|
+
/**
|
|
17
|
+
* Split text into terms. Defaults to lowercase alphanumeric runs. Provide your
|
|
18
|
+
* own for stemming, n-grams, or a different alphabet — used for both indexing
|
|
19
|
+
* and querying, so the two always agree.
|
|
20
|
+
*/
|
|
21
|
+
tokenize?: (text: string) => string[];
|
|
22
|
+
/** Terms to drop (e.g. `the`, `a`). Applied after `tokenize`. */
|
|
23
|
+
stopwords?: Iterable<string>;
|
|
24
|
+
/** BM25 term-frequency saturation. Defaults to 1.5. */
|
|
25
|
+
k1?: number;
|
|
26
|
+
/** BM25 length normalization (0–1). Defaults to 0.75. */
|
|
27
|
+
b?: number;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Build an incremental BM25 full-text index over rows of `T`. Implements the
|
|
31
|
+
* {@link SearchIndex} interface, so it plugs straight into a search collection.
|
|
32
|
+
*/
|
|
33
|
+
export declare const createTextIndex: <T>(options: TextIndexOptions<T>) => SearchIndex<T, string>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RowKey } from './types';
|
|
2
|
+
import type { SearchIndex } from './search';
|
|
3
|
+
/**
|
|
4
|
+
* An incremental vector index for semantic / similarity search — the embeddings
|
|
5
|
+
* half of the search surface (see {@link createTextIndex} for keyword search).
|
|
6
|
+
* Pure and dependency-free: an exact (brute-force) k-NN over in-memory vectors,
|
|
7
|
+
* maintained as rows are added/removed, so a {@link defineSearchCollection}
|
|
8
|
+
* stays live. Pairs naturally with `@absolutejs/ai` / `@absolutejs/rag` for RAG
|
|
9
|
+
* retrieval on your own data. Exact search is O(n·d) per query — fine for tens
|
|
10
|
+
* of thousands of vectors; for more, back it with pgvector and a real ANN index.
|
|
11
|
+
*/
|
|
12
|
+
/** Similarity metric. `cosine`/`dot` rank higher = closer; `euclidean` too (negated distance). */
|
|
13
|
+
export type VectorMetric = 'cosine' | 'dot' | 'euclidean';
|
|
14
|
+
export type VectorIndexOptions<T> = {
|
|
15
|
+
/** Row identity. */
|
|
16
|
+
key: (row: T) => RowKey;
|
|
17
|
+
/** Extract a row's embedding vector. */
|
|
18
|
+
embedding: (row: T) => number[];
|
|
19
|
+
/** Similarity metric. Defaults to `cosine`. */
|
|
20
|
+
metric?: VectorMetric;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Build an incremental vector index over rows of `T`. Implements the
|
|
24
|
+
* {@link SearchIndex} interface (queried by a query vector), so it plugs
|
|
25
|
+
* straight into a search collection.
|
|
26
|
+
*/
|
|
27
|
+
export declare const createVectorIndex: <T>(options: VectorIndexOptions<T>) => SearchIndex<T, number[]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Lightweight reactive-push and write-behind-cache primitives for Elysia and the AbsoluteJS ecosystem — kill polling and keep a remote store off your hot path, without adopting a whole sync-engine backend.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|