@absolutejs/sync 0.10.0 → 0.12.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 +50 -46
- package/dist/angular/index.js +269 -1
- package/dist/angular/index.js.map +7 -5
- package/dist/angular/sync-collection.service.d.ts +14 -0
- package/dist/client/collaborativeText.d.ts +51 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +246 -2
- package/dist/client/index.js.map +5 -3
- package/dist/crdt/index.d.ts +21 -4
- package/dist/crdt/index.js +12 -3
- package/dist/crdt/index.js.map +3 -3
- package/dist/engine/index.d.ts +2 -1
- package/dist/engine/index.js +45 -12
- package/dist/engine/index.js.map +3 -3
- package/dist/engine/syncEngine.d.ts +20 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +270 -2
- package/dist/react/index.js.map +7 -4
- package/dist/react/useCollaborativeText.d.ts +17 -0
- package/dist/svelte/createCollaborativeTextStore.d.ts +15 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +279 -2
- package/dist/svelte/index.js.map +7 -4
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +273 -2
- package/dist/vue/index.js.map +7 -4
- package/dist/vue/useCollaborativeText.d.ts +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -345,30 +345,33 @@ it, ~3 store round-trips every 20ms ran the voice pipeline far slower than real
|
|
|
345
345
|
|
|
346
346
|
### `@absolutejs/sync/client`
|
|
347
347
|
|
|
348
|
-
| Export
|
|
349
|
-
|
|
|
350
|
-
| `createSyncSubscriber({ topics, onEvent, url? })`
|
|
351
|
-
| `createLiveQuery({ topics, fetcher, ... })`
|
|
352
|
-
| `jsonFetcher(url, init?)`
|
|
353
|
-
| `createSyncCollection({ url, collection, ... })`
|
|
354
|
-
| `createSyncClient({ url })`
|
|
355
|
-
| `createPresence({ url, room, state })`
|
|
356
|
-
| `
|
|
357
|
-
| `
|
|
358
|
-
| `
|
|
348
|
+
| Export | What it is |
|
|
349
|
+
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
350
|
+
| `createSyncSubscriber({ topics, onEvent, url? })` | Browser SSE client. |
|
|
351
|
+
| `createLiveQuery({ topics, fetcher, ... })` | Hydrate-once, refetch-on-event observable query store. |
|
|
352
|
+
| `jsonFetcher(url, init?)` | Default `fetcher`: GET + JSON parse, forwards the abort signal. |
|
|
353
|
+
| `createSyncCollection({ url, collection, ... })` | Live diff-driven collection store with optimistic `mutate`. |
|
|
354
|
+
| `createSyncClient({ url })` | One socket, many collections (`client.collection(...)`). Applies a multi-collection mutation's diffs as one **consistent frame** — no torn cross-collection paint. |
|
|
355
|
+
| `createPresence({ url, room, state })` | Join a presence room: see who's online / typing (`get` + `subscribe`) and publish your own state (`set`). |
|
|
356
|
+
| `createCollaborativeText({ url, collection, id, field, ... })` | Live CRDT collaborative-text controller (`get`/`subscribe`/`setText`/`close`): tracks a row's CRDT field, merges remote edits into a local replica, and broadcasts via the engine's `"<collection>:merge"` mutation. Backs the `useCollaborativeText` framework hooks. |
|
|
357
|
+
| `localStorageMutationStorage(key)` | `localStorage`-backed offline write queue for `createSyncCollection`. |
|
|
358
|
+
| `localStorageCollectionCache(key)` | `localStorage`-backed local-first read cache: confirmed rows survive a reload, resume from the cached version. |
|
|
359
|
+
| `indexedDbCollectionCache({ key, ... })` | IndexedDB-backed local-first read cache — durable, large-capacity. Same resume semantics, async storage. |
|
|
359
360
|
|
|
360
361
|
### Framework bindings — `@absolutejs/sync/{react,vue,svelte,angular}`
|
|
361
362
|
|
|
362
363
|
Idiomatic wrappers over `createSyncCollection`, one per framework, so a live
|
|
363
364
|
collection is one call. Each returns the same `{ data, status, error, mutate }`
|
|
364
|
-
and is SSR-safe (the socket opens on the client only).
|
|
365
|
+
and is SSR-safe (the socket opens on the client only). Each also ships a
|
|
366
|
+
**collaborative-text** binding over `createCollaborativeText` — a CRDT shared text
|
|
367
|
+
field in one call (`text`/`setText`/`status`).
|
|
365
368
|
|
|
366
|
-
| Subpath |
|
|
367
|
-
| ---------- | ---------------------------------------- |
|
|
368
|
-
| `/react` | `useSyncCollection(options)` |
|
|
369
|
-
| `/vue` | `useSyncCollection(options)` |
|
|
370
|
-
| `/svelte` | `createSyncCollectionStore(options)` |
|
|
371
|
-
| `/angular` | `SyncCollectionService.connect(options)` |
|
|
369
|
+
| Subpath | Collection | Collaborative text |
|
|
370
|
+
| ---------- | ---------------------------------------- | ----------------------------------------------- |
|
|
371
|
+
| `/react` | `useSyncCollection(options)` | `useCollaborativeText(options)` |
|
|
372
|
+
| `/vue` | `useSyncCollection(options)` | `useCollaborativeText(options)` |
|
|
373
|
+
| `/svelte` | `createSyncCollectionStore(options)` | `createCollaborativeTextStore(options)` |
|
|
374
|
+
| `/angular` | `SyncCollectionService.connect(options)` | `SyncCollectionService.collaborativeText(opts)` |
|
|
372
375
|
|
|
373
376
|
```tsx
|
|
374
377
|
// React
|
|
@@ -389,38 +392,39 @@ mutate({
|
|
|
389
392
|
|
|
390
393
|
### `@absolutejs/sync/engine`
|
|
391
394
|
|
|
392
|
-
| Export | What it is
|
|
393
|
-
| ---------------------------------------------------------------------------------------- |
|
|
394
|
-
| `createSyncEngine()` | Registry + view syncer: `register`, `subscribe`, `applyChange`, `connectSource`, `registerMutation`, `registerWriter`, `runMutation`.
|
|
395
|
-
| `defineCollection({ name, hydrate, key?, match?, authorize?, tables? })` | Define a syncable collection.
|
|
396
|
-
| `defineMutation({ name, handler, authorize? })` | Define a server mutation. Its `handler` gets `actions.insert/update/delete` (write through a registered `TableWriter` → persists + emits in one step) plus `actions.change` (escape hatch). Changes commit atomically.
|
|
397
|
-
| `registerWriter(table, { insert, update, delete })` | Teach the engine how to persist a table (any ORM), so writes auto-emit — you can't write without going live.
|
|
398
|
-
| `createAggregate({ key, groupBy?, value? })` | Incremental count/sum/avg/min/max by group.
|
|
399
|
-
| `createMaterializedView({ key, match, equals? })` | The predicate-matching IVM primitive (`apply`/`reset` → diffs).
|
|
400
|
-
| `createPollingChangeSource({ poll, intervalMs?, startSeq?, onProcessed? })` | DB-agnostic CDC `ChangeSource` that tails a changelog (outbox) table.
|
|
401
|
-
| `engine.connectCluster(bus)` + `createInMemoryClusterBus()` | Horizontal scale: fan changes across server instances over a `ClusterBus` (BYO Redis/Postgres; in-memory bus for dev).
|
|
402
|
-
| `createPresenceHub()` + `syncSocket({ engine, presence })` | Ephemeral room-scoped presence (online / typing / cursors) over the same socket — not persisted, auto-cleaned on disconnect.
|
|
403
|
-
| `query(source).filter().map().join().leftJoin().groupBy().orderBy()` | Declarative incremental query builder (the operator graph).
|
|
404
|
-
| `defineGraphCollection({ name, query, key, authorize? })` | Run a `query` as a live collection.
|
|
405
|
-
| `defineReactiveQuery({ name, run, key })` + `registerReactive` / `registerReader` | Read-set-tracked query: `run(ctx)` reads via `ctx.db` (`all`/`get`/`where`) and re-runs only when the rows/ranges it read change — no `match`, no manual emit.
|
|
406
|
-
| `definePermissions({ [table]: { read?, insert?, update?, delete?, write? } })` | Declarative row-level access control. Pass as `createSyncEngine({ permissions })` or `registerPermissions(table, rules)`. Read rules filter every row emitted; write rules gate `actions.insert/update/delete`.
|
|
407
|
-
| `defineSchema({ [table]: { fields, version?, migrate? } })` + `field` kit | Declarative row schema. Pass as `createSyncEngine({ schemas })` or `registerSchema(table, schema)`. Writes are validated (bad write → `SchemaError`); `migrate` lazily upcasts rows on read (no DB migration needed).
|
|
408
|
-
| `
|
|
409
|
-
| `
|
|
410
|
-
| `
|
|
411
|
-
| `
|
|
395
|
+
| Export | What it is |
|
|
396
|
+
| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
397
|
+
| `createSyncEngine()` | Registry + view syncer: `register`, `subscribe`, `applyChange`, `connectSource`, `registerMutation`, `registerWriter`, `runMutation`. |
|
|
398
|
+
| `defineCollection({ name, hydrate, key?, match?, authorize?, tables? })` | Define a syncable collection. |
|
|
399
|
+
| `defineMutation({ name, handler, authorize? })` | Define a server mutation. Its `handler` gets `actions.insert/update/delete` (write through a registered `TableWriter` → persists + emits in one step) plus `actions.change` (escape hatch). Changes commit atomically. |
|
|
400
|
+
| `registerWriter(table, { insert, update, delete })` | Teach the engine how to persist a table (any ORM), so writes auto-emit — you can't write without going live. |
|
|
401
|
+
| `createAggregate({ key, groupBy?, value? })` | Incremental count/sum/avg/min/max by group. |
|
|
402
|
+
| `createMaterializedView({ key, match, equals? })` | The predicate-matching IVM primitive (`apply`/`reset` → diffs). |
|
|
403
|
+
| `createPollingChangeSource({ poll, intervalMs?, startSeq?, onProcessed? })` | DB-agnostic CDC `ChangeSource` that tails a changelog (outbox) table. |
|
|
404
|
+
| `engine.connectCluster(bus)` + `createInMemoryClusterBus()` | Horizontal scale: fan changes across server instances over a `ClusterBus` (BYO Redis/Postgres; in-memory bus for dev). |
|
|
405
|
+
| `createPresenceHub()` + `syncSocket({ engine, presence })` | Ephemeral room-scoped presence (online / typing / cursors) over the same socket — not persisted, auto-cleaned on disconnect. |
|
|
406
|
+
| `query(source).filter().map().join().leftJoin().groupBy().orderBy()` | Declarative incremental query builder (the operator graph). |
|
|
407
|
+
| `defineGraphCollection({ name, query, key, authorize? })` | Run a `query` as a live collection. |
|
|
408
|
+
| `defineReactiveQuery({ name, run, key })` + `registerReactive` / `registerReader` | Read-set-tracked query: `run(ctx)` reads via `ctx.db` (`all`/`get`/`where`) and re-runs only when the rows/ranges it read change — no `match`, no manual emit. |
|
|
409
|
+
| `definePermissions({ [table]: { read?, insert?, update?, delete?, write? } })` | Declarative row-level access control. Pass as `createSyncEngine({ permissions })` or `registerPermissions(table, rules)`. Read rules filter every row emitted; write rules gate `actions.insert/update/delete`. |
|
|
410
|
+
| `defineSchema({ [table]: { fields, version?, migrate? } })` + `field` kit | Declarative row schema. Pass as `createSyncEngine({ schemas })` or `registerSchema(table, schema)`. Writes are validated (bad write → `SchemaError`); `migrate` lazily upcasts rows on read (no DB migration needed). |
|
|
411
|
+
| `registerCrdt(table, { [field]: mergeable })` | Declare CRDT fields (a `CrdtMergeable` like `rgaText`, or `yjsText` from `@absolutejs/sync-yjs`). The engine **merges** those fields on `actions.insert/update` instead of overwriting — conflict-free collaborative editing with no merge code — and auto-registers a `"<table>:merge"` mutation the `useCollaborativeText` hooks call. |
|
|
412
|
+
| `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`. |
|
|
413
|
+
| `createTextIndex({ key, fields, tokenize?, stopwords?, k1?, b? })` | Incremental BM25 full-text index (keyword search). Implements `SearchIndex`; usable standalone or inside a search collection. |
|
|
414
|
+
| `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. |
|
|
415
|
+
| `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). |
|
|
412
416
|
|
|
413
417
|
### `@absolutejs/sync/crdt`
|
|
414
418
|
|
|
415
|
-
Conflict-free replicated data types — pure, **zero-dependency**, and isomorphic (same code client and server). They merge concurrent edits from different tabs/devices without a server round-trip per keystroke and without clobbering: every `merge` is commutative, associative, and idempotent, so replicas converge no matter the order. They ride the existing engine with no engine changes — store the CRDT state as a row field
|
|
419
|
+
Conflict-free replicated data types — pure, **zero-dependency**, and isomorphic (same code client and server). They merge concurrent edits from different tabs/devices without a server round-trip per keystroke and without clobbering: every `merge` is commutative, associative, and idempotent, so replicas converge no matter the order. They ride the existing engine with no engine changes — store the CRDT state as a row field. The declarative path is one line each end: server `engine.registerCrdt(table, { field: rgaText })` (auto-merges that field on write), client `useCollaborativeText({ collection, id, field, url })`. The primitives below are also usable directly.
|
|
416
420
|
|
|
417
|
-
| Export | What it is
|
|
418
|
-
| -------------------------------------------- |
|
|
419
|
-
| `counter` | PN-counter: `create/value/increment/decrement/merge`. Concurrent increments and decrements across replicas all survive.
|
|
420
|
-
| `lww` | Last-write-wins register: `create/set/merge`. The latest timestamp wins (replica id breaks ties) — for "just take the newest value" fields.
|
|
421
|
-
| `createTextCrdt(replica, initial?)` | Collaborative text (an RGA sequence CRDT): `text/insert/delete/setText/merge/state`. Drive it
|
|
422
|
-
| `textOf(state)` / `mergeTextState(a, b)` | Pure helpers for the text state — use them server-side (e.g. a merge-on-write mutation) with no live instance.
|
|
423
|
-
| `CrdtText<State>` / `TextCrdtAdapter<State>` | The pluggable collaborative-text contract. `rgaText` is the first-party (zero-dep) backend; swap in an adapter from the `sync-adapters` repo (e.g. `@absolutejs/sync-yjs`, which wraps the Yjs staple) behind the same call sites.
|
|
421
|
+
| Export | What it is |
|
|
422
|
+
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
423
|
+
| `counter` | PN-counter: `create/value/increment/decrement/merge`. Concurrent increments and decrements across replicas all survive. |
|
|
424
|
+
| `lww` | Last-write-wins register: `create/set/merge`. The latest timestamp wins (replica id breaks ties) — for "just take the newest value" fields. |
|
|
425
|
+
| `createTextCrdt(replica, initial?)` | Collaborative text (an RGA sequence CRDT): `text/insert/delete/setText/merge/state` + `takeDelta`. Drive it via `setText`; apply remote state via `merge`; `takeDelta()` returns just this client's new ops (a delta-state) so uploads are O(edit), not O(doc). Concurrent edits merge and converge. |
|
|
426
|
+
| `textOf(state)` / `mergeTextState(a, b)` | Pure helpers for the text state — use them server-side (e.g. a merge-on-write mutation) with no live instance. |
|
|
427
|
+
| `CrdtText<State>` / `TextCrdtAdapter<State>` | The pluggable collaborative-text contract. `rgaText` is the first-party (zero-dep) backend; swap in an adapter from the `sync-adapters` repo (e.g. `@absolutejs/sync-yjs`, which wraps the Yjs staple) behind the same call sites. |
|
|
424
428
|
|
|
425
429
|
### `@absolutejs/sync/postgres`
|
|
426
430
|
|
package/dist/angular/index.js
CHANGED
|
@@ -68,6 +68,191 @@ var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
|
68
68
|
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
// src/crdt/index.ts
|
|
72
|
+
var sumValues = (counts) => Object.values(counts).reduce((total, value) => total + value, 0);
|
|
73
|
+
var mergeMax = (a, b) => {
|
|
74
|
+
const merged = { ...a };
|
|
75
|
+
for (const [replica, value] of Object.entries(b)) {
|
|
76
|
+
merged[replica] = Math.max(merged[replica] ?? 0, value);
|
|
77
|
+
}
|
|
78
|
+
return merged;
|
|
79
|
+
};
|
|
80
|
+
var counter = {
|
|
81
|
+
create: () => ({ increments: {}, decrements: {} }),
|
|
82
|
+
value: (state) => sumValues(state.increments) - sumValues(state.decrements),
|
|
83
|
+
increment: (state, replica, by = 1) => ({
|
|
84
|
+
increments: {
|
|
85
|
+
...state.increments,
|
|
86
|
+
[replica]: (state.increments[replica] ?? 0) + by
|
|
87
|
+
},
|
|
88
|
+
decrements: state.decrements
|
|
89
|
+
}),
|
|
90
|
+
decrement: (state, replica, by = 1) => ({
|
|
91
|
+
increments: state.increments,
|
|
92
|
+
decrements: {
|
|
93
|
+
...state.decrements,
|
|
94
|
+
[replica]: (state.decrements[replica] ?? 0) + by
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
merge: (a, b) => ({
|
|
98
|
+
increments: mergeMax(a.increments, b.increments),
|
|
99
|
+
decrements: mergeMax(a.decrements, b.decrements)
|
|
100
|
+
})
|
|
101
|
+
};
|
|
102
|
+
var lww = {
|
|
103
|
+
create: (value, replica, timestamp = Date.now()) => ({ value, timestamp, replica }),
|
|
104
|
+
set: (value, replica, timestamp = Date.now()) => ({
|
|
105
|
+
value,
|
|
106
|
+
timestamp,
|
|
107
|
+
replica
|
|
108
|
+
}),
|
|
109
|
+
merge: (a, b) => {
|
|
110
|
+
if (b.timestamp > a.timestamp) {
|
|
111
|
+
return b;
|
|
112
|
+
}
|
|
113
|
+
if (b.timestamp < a.timestamp) {
|
|
114
|
+
return a;
|
|
115
|
+
}
|
|
116
|
+
return b.replica > a.replica ? b : a;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var compare = (a, b) => {
|
|
120
|
+
if (a.clock !== b.clock) {
|
|
121
|
+
return b.clock - a.clock;
|
|
122
|
+
}
|
|
123
|
+
if (a.replica === b.replica) {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
return a.replica > b.replica ? -1 : 1;
|
|
127
|
+
};
|
|
128
|
+
var linearize = (elements) => {
|
|
129
|
+
const children = new Map;
|
|
130
|
+
for (const element of elements) {
|
|
131
|
+
const list = children.get(element.after);
|
|
132
|
+
if (list === undefined) {
|
|
133
|
+
children.set(element.after, [element]);
|
|
134
|
+
} else {
|
|
135
|
+
list.push(element);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const list of children.values()) {
|
|
139
|
+
list.sort(compare);
|
|
140
|
+
}
|
|
141
|
+
const ordered = [];
|
|
142
|
+
const stack = [...children.get(null) ?? []].reverse();
|
|
143
|
+
while (stack.length > 0) {
|
|
144
|
+
const element = stack.pop();
|
|
145
|
+
ordered.push(element);
|
|
146
|
+
const kids = children.get(element.id);
|
|
147
|
+
if (kids !== undefined) {
|
|
148
|
+
for (let index = kids.length - 1;index >= 0; index -= 1) {
|
|
149
|
+
stack.push(kids[index]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return ordered;
|
|
154
|
+
};
|
|
155
|
+
var textOf = (state) => linearize(state.elements).filter((element) => !element.deleted).map((element) => element.value).join("");
|
|
156
|
+
var mergeTextState = (a, b) => {
|
|
157
|
+
const byId = new Map;
|
|
158
|
+
for (const element of [...a.elements, ...b.elements]) {
|
|
159
|
+
const existing = byId.get(element.id);
|
|
160
|
+
byId.set(element.id, existing === undefined ? element : { ...existing, deleted: existing.deleted || element.deleted });
|
|
161
|
+
}
|
|
162
|
+
return { elements: [...byId.values()] };
|
|
163
|
+
};
|
|
164
|
+
var createTextCrdt = (replica, initial) => {
|
|
165
|
+
const elements = new Map;
|
|
166
|
+
const pending = new Map;
|
|
167
|
+
let clock = 0;
|
|
168
|
+
if (initial !== undefined) {
|
|
169
|
+
for (const element of initial.elements) {
|
|
170
|
+
elements.set(element.id, element);
|
|
171
|
+
clock = Math.max(clock, element.clock);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const visible = () => linearize([...elements.values()]).filter((element) => !element.deleted);
|
|
175
|
+
const insert = (index, value) => {
|
|
176
|
+
const seen = visible();
|
|
177
|
+
let after = index <= 0 ? null : seen[index - 1]?.id ?? null;
|
|
178
|
+
for (const char of [...value]) {
|
|
179
|
+
clock += 1;
|
|
180
|
+
const element = {
|
|
181
|
+
id: `${replica}:${clock}`,
|
|
182
|
+
replica,
|
|
183
|
+
clock,
|
|
184
|
+
after,
|
|
185
|
+
value: char,
|
|
186
|
+
deleted: false
|
|
187
|
+
};
|
|
188
|
+
elements.set(element.id, element);
|
|
189
|
+
pending.set(element.id, element);
|
|
190
|
+
after = element.id;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const remove = (index, count) => {
|
|
194
|
+
const seen = visible();
|
|
195
|
+
for (let offset = 0;offset < count; offset += 1) {
|
|
196
|
+
const target = seen[index + offset];
|
|
197
|
+
if (target !== undefined) {
|
|
198
|
+
const tombstoned = { ...target, deleted: true };
|
|
199
|
+
elements.set(target.id, tombstoned);
|
|
200
|
+
pending.set(target.id, tombstoned);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
text: () => textOf({ elements: [...elements.values()] }),
|
|
206
|
+
insert,
|
|
207
|
+
delete: remove,
|
|
208
|
+
merge: (state) => {
|
|
209
|
+
for (const element of state.elements) {
|
|
210
|
+
const existing = elements.get(element.id);
|
|
211
|
+
elements.set(element.id, existing === undefined ? element : {
|
|
212
|
+
...existing,
|
|
213
|
+
deleted: existing.deleted || element.deleted
|
|
214
|
+
});
|
|
215
|
+
clock = Math.max(clock, element.clock);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
setText: (next) => {
|
|
219
|
+
const current = textOf({ elements: [...elements.values()] });
|
|
220
|
+
if (current === next) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
let prefix = 0;
|
|
224
|
+
const maxPrefix = Math.min(current.length, next.length);
|
|
225
|
+
while (prefix < maxPrefix && current[prefix] === next[prefix]) {
|
|
226
|
+
prefix += 1;
|
|
227
|
+
}
|
|
228
|
+
let suffix = 0;
|
|
229
|
+
while (suffix < maxPrefix - prefix && current[current.length - 1 - suffix] === next[next.length - 1 - suffix]) {
|
|
230
|
+
suffix += 1;
|
|
231
|
+
}
|
|
232
|
+
const removed = current.length - prefix - suffix;
|
|
233
|
+
if (removed > 0) {
|
|
234
|
+
remove(prefix, removed);
|
|
235
|
+
}
|
|
236
|
+
const inserted = next.slice(prefix, next.length - suffix);
|
|
237
|
+
if (inserted.length > 0) {
|
|
238
|
+
insert(prefix, inserted);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
state: () => ({ elements: [...elements.values()] }),
|
|
242
|
+
takeDelta: () => {
|
|
243
|
+
const delta = { elements: [...pending.values()] };
|
|
244
|
+
pending.clear();
|
|
245
|
+
return delta;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
var rgaText = {
|
|
250
|
+
create: createTextCrdt,
|
|
251
|
+
empty: () => ({ elements: [] }),
|
|
252
|
+
merge: mergeTextState,
|
|
253
|
+
textOf
|
|
254
|
+
};
|
|
255
|
+
|
|
71
256
|
// src/angular/sync-collection.service.ts
|
|
72
257
|
import { computed, Injectable, signal } from "@angular/core";
|
|
73
258
|
|
|
@@ -390,6 +575,65 @@ var createSyncCollection = (options) => {
|
|
|
390
575
|
};
|
|
391
576
|
};
|
|
392
577
|
|
|
578
|
+
// src/client/collaborativeText.ts
|
|
579
|
+
var createCollaborativeText = (options) => {
|
|
580
|
+
const keyField = options.keyField ?? "id";
|
|
581
|
+
const mutation = options.mutation ?? `${options.collection}:merge`;
|
|
582
|
+
const replica = options.replica ?? globalThis.crypto.randomUUID();
|
|
583
|
+
const make = options.create ?? ((id) => createTextCrdt(id));
|
|
584
|
+
const crdt = make(replica);
|
|
585
|
+
let current = { status: "connecting", text: "" };
|
|
586
|
+
const subscribers = new Set;
|
|
587
|
+
const emit = () => {
|
|
588
|
+
for (const run of subscribers) {
|
|
589
|
+
run(current);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
const collection = createSyncCollection({
|
|
593
|
+
collection: options.collection,
|
|
594
|
+
key: (row) => row[keyField],
|
|
595
|
+
url: options.url
|
|
596
|
+
});
|
|
597
|
+
const apply = (state) => {
|
|
598
|
+
let { text } = current;
|
|
599
|
+
const row = state.data.find((candidate) => candidate[keyField] === options.id);
|
|
600
|
+
const fieldState = row?.[options.field];
|
|
601
|
+
if (fieldState !== undefined) {
|
|
602
|
+
crdt.merge(fieldState);
|
|
603
|
+
text = crdt.text();
|
|
604
|
+
}
|
|
605
|
+
current = { status: state.status, text };
|
|
606
|
+
emit();
|
|
607
|
+
};
|
|
608
|
+
apply(collection.get());
|
|
609
|
+
const unsubscribe = collection.subscribe(apply);
|
|
610
|
+
return {
|
|
611
|
+
get: () => current,
|
|
612
|
+
subscribe(run) {
|
|
613
|
+
subscribers.add(run);
|
|
614
|
+
run(current);
|
|
615
|
+
return () => {
|
|
616
|
+
subscribers.delete(run);
|
|
617
|
+
};
|
|
618
|
+
},
|
|
619
|
+
setText(next) {
|
|
620
|
+
crdt.setText(next);
|
|
621
|
+
current = { status: current.status, text: next };
|
|
622
|
+
emit();
|
|
623
|
+
const payload = crdt.takeDelta ? crdt.takeDelta() : crdt.state();
|
|
624
|
+
collection.mutate({
|
|
625
|
+
args: { [keyField]: options.id, [options.field]: payload },
|
|
626
|
+
name: mutation
|
|
627
|
+
});
|
|
628
|
+
},
|
|
629
|
+
close() {
|
|
630
|
+
unsubscribe();
|
|
631
|
+
collection.close();
|
|
632
|
+
subscribers.clear();
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
};
|
|
636
|
+
|
|
393
637
|
// src/angular/sync-collection.service.ts
|
|
394
638
|
var _dec = [
|
|
395
639
|
Injectable({ providedIn: "root" })
|
|
@@ -398,6 +642,7 @@ var _init = __decoratorStart(undefined);
|
|
|
398
642
|
|
|
399
643
|
class SyncCollectionService {
|
|
400
644
|
collections = new Set;
|
|
645
|
+
texts = new Set;
|
|
401
646
|
connect(options) {
|
|
402
647
|
const data = signal([]);
|
|
403
648
|
const status = signal("connecting");
|
|
@@ -422,11 +667,34 @@ class SyncCollectionService {
|
|
|
422
667
|
status: computed(() => status())
|
|
423
668
|
};
|
|
424
669
|
}
|
|
670
|
+
collaborativeText(options) {
|
|
671
|
+
const text = signal("");
|
|
672
|
+
const status = signal("connecting");
|
|
673
|
+
let controller = null;
|
|
674
|
+
if (typeof window !== "undefined") {
|
|
675
|
+
controller = createCollaborativeText(options);
|
|
676
|
+
this.texts.add(controller);
|
|
677
|
+
controller.subscribe((state) => {
|
|
678
|
+
text.set(state.text);
|
|
679
|
+
status.set(state.status);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const setText = (next) => controller?.setText(next);
|
|
683
|
+
return {
|
|
684
|
+
setText,
|
|
685
|
+
status: computed(() => status()),
|
|
686
|
+
text: computed(() => text())
|
|
687
|
+
};
|
|
688
|
+
}
|
|
425
689
|
ngOnDestroy() {
|
|
426
690
|
for (const collection of this.collections) {
|
|
427
691
|
collection.close();
|
|
428
692
|
}
|
|
429
693
|
this.collections.clear();
|
|
694
|
+
for (const text of this.texts) {
|
|
695
|
+
text.close();
|
|
696
|
+
}
|
|
697
|
+
this.texts.clear();
|
|
430
698
|
}
|
|
431
699
|
}
|
|
432
700
|
SyncCollectionService = __decorateElement(_init, 0, "SyncCollectionService", _dec, SyncCollectionService);
|
|
@@ -437,5 +705,5 @@ export {
|
|
|
437
705
|
SyncCollectionService
|
|
438
706
|
};
|
|
439
707
|
|
|
440
|
-
//# debugId=
|
|
708
|
+
//# debugId=248B59EB4C19056664756E2164756E21
|
|
441
709
|
//# sourceMappingURL=index.js.map
|