@luckystack/sync 0.1.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/CHANGELOG.md +14 -0
- package/CLAUDE.md +104 -0
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/client.d.ts +126 -0
- package/dist/client.js +537 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +1203 -0
- package/dist/index.js.map +1 -0
- package/docs/callback-registration.md +257 -0
- package/docs/error-states.md +252 -0
- package/docs/ignore-self.md +162 -0
- package/docs/room-fanout.md +233 -0
- package/docs/server-vs-client-handlers.md +321 -0
- package/docs/streaming.md +349 -0
- package/docs/sync-request.md +284 -0
- package/docs/version-policy.md +362 -0
- package/package.json +75 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@luckystack/sync` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial public release as part of the LuckyStack package split.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @luckystack/sync
|
|
2
|
+
|
|
3
|
+
> AI summary + function INDEX. For deep specs see `docs/` next to this file.
|
|
4
|
+
|
|
5
|
+
## What this package does
|
|
6
|
+
|
|
7
|
+
Real-time sync transport for LuckyStack. Provides type-safe, room-based fanout over Socket.io with an HTTP/SSE fallback. Each sync event is a file-based route with a mandatory `_server_v{N}.ts` (runs once, validates, produces `serverOutput`) and an optional `_client_v{N}.ts` (runs once per recipient socket for per-target filtering or auth). Streaming primitives (`stream`, `broadcastStream`, `streamTo`) support live LLM tokens, collab-editor diffs, and per-recipient progress. An offline queue with configurable drop policy keeps optimistic sends from being lost when the socket reconnects.
|
|
8
|
+
|
|
9
|
+
## When to USE this package
|
|
10
|
+
|
|
11
|
+
- Real-time room-based fanout (collab editors, chat, multiplayer state)
|
|
12
|
+
- Streaming AI/LLM tokens with throttled chunking
|
|
13
|
+
- Per-recipient customization (filter / translate / brand) without a second round-trip
|
|
14
|
+
- Optimistic offline sends that should replay when the socket reconnects
|
|
15
|
+
- Server-validated mutations that must be broadcast to every peer in a room
|
|
16
|
+
|
|
17
|
+
## When to NOT suggest this (yet)
|
|
18
|
+
|
|
19
|
+
- Request/response with no fanout — use `@luckystack/api` (`apiRequest`)
|
|
20
|
+
- Pure presence/online-status broadcasts — use `@luckystack/presence`
|
|
21
|
+
- Cross-instance fanout without a Redis adapter — wire the adapter first (see `/docs/ARCHITECTURE_SOCKET.md`)
|
|
22
|
+
- One-off background jobs unrelated to a client room
|
|
23
|
+
- HTTP-only environments without a long-lived connection AND no SSE — the package needs at minimum the HTTP/SSE fallback
|
|
24
|
+
|
|
25
|
+
## Function Index
|
|
26
|
+
|
|
27
|
+
### Server entry (`@luckystack/sync`)
|
|
28
|
+
|
|
29
|
+
| Export | One-liner | Deep doc |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `handleSyncRequest({ msg, socket, token })` | Socket.io sync entry — auth, rate-limit, validate, run `_server`, fanout, run `_client` per recipient, emit results, dispatch hooks. | → `docs/server-vs-client-handlers.md` |
|
|
32
|
+
| `handleHttpSyncRequest(req, res)` | HTTP/SSE fallback for sync requests when websockets are blocked. | → `docs/sync-request.md` |
|
|
33
|
+
| `createStreamThrottle({ flushEveryMs?, flushAtChars?, field? })` | Coalesce tiny stream pieces (LLM tokens) into bigger chunks before emit. | → `docs/streaming.md` |
|
|
34
|
+
| Type: `HttpSyncStreamEvent` | SSE event shape emitted by the HTTP fallback. | → `docs/sync-request.md` |
|
|
35
|
+
| Type: `StreamThrottle` / `CreateStreamThrottleOptions` | Throttle handle + options. | → `docs/streaming.md` |
|
|
36
|
+
|
|
37
|
+
### Client entry (`@luckystack/sync/client`)
|
|
38
|
+
|
|
39
|
+
| Export | One-liner | Deep doc |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `syncRequest({ name, version, data?, receiver, ignoreSelf?, onStream?, offlineDropPolicy? })` | Fire a typed sync event into a room; resolves with the server result envelope. | → `docs/sync-request.md` |
|
|
42
|
+
| `useSyncEvents()` | React hook returning `{ upsertSyncEventCallback, upsertSyncEventStreamCallback }` scoped to the component lifetime. | → `docs/callback-registration.md` |
|
|
43
|
+
| `useSyncEventTrigger()` | React hook returning `{ triggerSyncEvent, triggerSyncStreamEvent }` for manually invoking registered callbacks (testing / local echo). | → `docs/callback-registration.md` |
|
|
44
|
+
| `initSyncRequest({ setSocketStatus, sessionRef })` | One-time wiring of socket lifecycle handlers (connect/disconnect/reconnectAttempt/userAfk/userBack/connectError) into the socket-status provider. | → `docs/callback-registration.md` |
|
|
45
|
+
| Type: `SyncRequestStreamEvent<T>` | Payload shape passed to `onStream` on the originator side. | → `docs/streaming.md` |
|
|
46
|
+
| Type: `SyncRouteStreamEvent<T>` | Payload shape passed to `upsertSyncEventStreamCallback` on recipients. | → `docs/streaming.md` |
|
|
47
|
+
|
|
48
|
+
### Streaming primitives (received inside `_server_v{N}.ts` params)
|
|
49
|
+
|
|
50
|
+
| Primitive | Audience | Deep doc |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `stream(payload)` | Originator socket only (cheapest). | → `docs/streaming.md` |
|
|
53
|
+
| `broadcastStream(payload)` | Every socket in `roomCode`, across all instances (`io.to(room).emit` via the Redis adapter). | → `docs/streaming.md` |
|
|
54
|
+
| `streamTo(tokens, payload)` | Selective fanout to specific session tokens. | → `docs/streaming.md` |
|
|
55
|
+
| `stream(payload)` in `_client_v{N}.ts` | Per-recipient, runs after `_server` finishes. | → `docs/streaming.md` |
|
|
56
|
+
|
|
57
|
+
### Hooks dispatched by the server handler
|
|
58
|
+
|
|
59
|
+
| Hook | When | Deep doc |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `preSyncAuthorize` | After basic `AuthProps` check, before rate-limit + input validation. Stop to reject. | → `docs/server-vs-client-handlers.md` |
|
|
62
|
+
| `preSyncFanout` | After `_server` runs, before any recipient receives the payload. Stop to abort fanout. | → `docs/room-fanout.md` |
|
|
63
|
+
| `postSyncFanout` | After all recipients have been emitted to. Receives `recipientCount`. | → `docs/room-fanout.md` |
|
|
64
|
+
| `rateLimitExceeded` | When the per-route or per-IP bucket rejects a sync. | → `docs/error-states.md` |
|
|
65
|
+
|
|
66
|
+
## Config keys
|
|
67
|
+
|
|
68
|
+
### `registerProjectConfig({ sync, offlineQueue })`
|
|
69
|
+
|
|
70
|
+
- `sync.streamThrottle.flushAtChars` — char threshold before a buffered throttle flushes. Default `32`.
|
|
71
|
+
- `sync.streamThrottle.flushEveryMs` — timer-based flush interval; `false` disables the timer. Default `50`.
|
|
72
|
+
- `sync.streamThrottle.field` — payload key carrying the buffered text. Default `'chunk'`.
|
|
73
|
+
- `sync.fanoutYieldEvery` — yield to the event loop every N recipients during a giant fanout.
|
|
74
|
+
- `sync.fanoutYieldMs` — duration of the yield `setTimeout`.
|
|
75
|
+
- `offlineQueue.maxSize` — cap on the client-side offline queue.
|
|
76
|
+
- `offlineQueue.dropPolicy` — `'reject'` (default — overflow returns `offline.queueFull`), `'drop-oldest'`, or `'drop-newest'`. Per-request override via `syncRequest({ offlineDropPolicy })`.
|
|
77
|
+
|
|
78
|
+
### Logging toggles
|
|
79
|
+
|
|
80
|
+
- `logging.devLogs`, `logging.devNotifications`, `logging.socketStatus`, `logging.stream` — drive the dev-only log lines in this package.
|
|
81
|
+
|
|
82
|
+
### Env vars
|
|
83
|
+
|
|
84
|
+
None directly. Inherits socket transport config from `@luckystack/server` and Redis adapter config from `@luckystack/core`.
|
|
85
|
+
|
|
86
|
+
## Peer dependencies
|
|
87
|
+
|
|
88
|
+
- **Required**: `@luckystack/core`, `@luckystack/login`, `@luckystack/error-tracking`
|
|
89
|
+
- **Peer (canonical ranges, 2026-05-07)**:
|
|
90
|
+
- `@prisma/client@^6.19.0` (transitively required via `@luckystack/core`)
|
|
91
|
+
- `react@^19.2.0` (only the `/client` subpath)
|
|
92
|
+
- `socket.io@^4.8.0` (server entry)
|
|
93
|
+
- `socket.io-client@^4.8.0` (client entry)
|
|
94
|
+
- Redis adapter (required for cross-instance fanout) wired by `@luckystack/server`; see `/docs/ARCHITECTURE_SOCKET.md`.
|
|
95
|
+
|
|
96
|
+
## Related
|
|
97
|
+
|
|
98
|
+
- Architecture deep-dive: `/docs/ARCHITECTURE_SYNC.md`
|
|
99
|
+
- Socket setup + Redis adapter: `/docs/ARCHITECTURE_SOCKET.md`
|
|
100
|
+
- Multi-instance model + pitfalls (regular `syncRequest` fan-out reaches across instances via `io.in(room).fetchSockets()` + `RemoteSocket.emit()`; streaming via `broadcastStream`/`streamTo`): `/docs/ARCHITECTURE_MULTI_INSTANCE.md`
|
|
101
|
+
- File-based `_sync/` routing: `/docs/ARCHITECTURE_ROUTING.md`
|
|
102
|
+
- Streaming page reconstruction: `/docs/STREAMING_RECONSTRUCTION.md`
|
|
103
|
+
- README (consumer quickstart): `./README.md`
|
|
104
|
+
- Sibling packages: `@luckystack/api`, `@luckystack/presence`, `@luckystack/server`
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mathijs van Melick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @luckystack/sync
|
|
2
|
+
|
|
3
|
+
> Real-time sync transport for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). Type-safe room-based fanout, server + per-client validation, streaming, optimistic offline queue. Server entry plus a browser-safe `./client` subpath for React.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @luckystack/sync @luckystack/core @luckystack/login @luckystack/error-tracking react socket.io socket.io-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
A sync event is two files (one mandatory, one optional):
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// src/board/_sync/moveCard_server_v1.ts — runs ONCE per request, validates and produces serverOutput
|
|
17
|
+
export const auth = { login: true };
|
|
18
|
+
|
|
19
|
+
export interface SyncParams {
|
|
20
|
+
data: { cardId: string; toLane: string };
|
|
21
|
+
user: SessionLayout;
|
|
22
|
+
receiver: string; // room code
|
|
23
|
+
functions: Functions;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const main = async ({ data, user, receiver }: SyncParams) => {
|
|
27
|
+
await prisma.card.update({ where: { id: data.cardId }, data: { laneId: data.toLane } });
|
|
28
|
+
return { status: 'success', serverOutput: { cardId: data.cardId, movedBy: user.id } };
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Add a `_client_v1.ts` only when you need per-client filtering, per-target auth, or a custom `clientOutput`. If it would just `return { status: 'success' }`, leave it out.
|
|
33
|
+
|
|
34
|
+
### Client side
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { syncRequest, upsertSyncEventCallback } from '@luckystack/sync/client';
|
|
38
|
+
|
|
39
|
+
upsertSyncEventCallback({
|
|
40
|
+
name: 'board/moveCard',
|
|
41
|
+
version: 'v1',
|
|
42
|
+
callback: ({ serverOutput, clientOutput }) => {
|
|
43
|
+
if (serverOutput.status !== 'success') return;
|
|
44
|
+
setCards(prev => moveCardLocally(prev, serverOutput.cardId));
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await syncRequest({
|
|
49
|
+
name: 'board/moveCard',
|
|
50
|
+
version: 'v1',
|
|
51
|
+
data: { cardId, toLane },
|
|
52
|
+
receiver: roomCode,
|
|
53
|
+
ignoreSelf: true,
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Subpaths
|
|
58
|
+
|
|
59
|
+
- `@luckystack/sync` — server-only transport adapters (`handleSyncRequest`, `handleHttpSyncRequest`). Wired by `@luckystack/server`.
|
|
60
|
+
- `@luckystack/sync/client` — browser-safe hooks (`syncRequest`, `useSyncEvents`, `upsertSyncEventCallback`). React 19 required.
|
|
61
|
+
|
|
62
|
+
## How it integrates
|
|
63
|
+
|
|
64
|
+
1. **Validates** server payload, then runs `_server_v{N}.ts` once.
|
|
65
|
+
2. **Dispatches** the `preSyncFanout` hook (may abort).
|
|
66
|
+
3. **Resolves** the room receiver list, optionally running `_client_v{N}.ts` once per recipient socket for per-client filtering or auth.
|
|
67
|
+
4. **Emits** the merged `{ serverOutput, clientOutput }` payload to each socket.
|
|
68
|
+
5. **Dispatches** `postSyncFanout` with the recipient count.
|
|
69
|
+
|
|
70
|
+
## Streaming
|
|
71
|
+
|
|
72
|
+
Sync handlers receive **four** stream primitives in their `_server` params, each picking a different audience and cost profile:
|
|
73
|
+
|
|
74
|
+
| Primitive | Audience | Use when |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `stream(payload)` | Originator only (cheapest) | Per-user progress nobody else cares about |
|
|
77
|
+
| `broadcastStream(payload)` | Everyone in `roomCode`, across all instances (Redis adapter) | Live AI chat tokens, collab editor diffs |
|
|
78
|
+
| `streamTo(tokens, payload)` | Specific session tokens | Selective subscribers (admin viewers, etc.) |
|
|
79
|
+
| `_client_v{N}.ts` `stream(...)` | Per-recipient (after `_server` finishes) | Per-target customization (filter / translate / brand) |
|
|
80
|
+
|
|
81
|
+
Plus `createStreamThrottle({ flushEveryMs, flushAtChars })` for coalescing tiny LLM tokens into bigger chunks — cuts message count by 10–100× without losing the "live" feel.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// src/chat/_sync/sendMessage_server_v1.ts — AI chat with live broadcast
|
|
85
|
+
import { createStreamThrottle } from '@luckystack/sync';
|
|
86
|
+
|
|
87
|
+
export const main = async ({ clientInput, broadcastStream }: SyncParams) => {
|
|
88
|
+
const throttle = createStreamThrottle({ flushEveryMs: 50, flushAtChars: 32 });
|
|
89
|
+
|
|
90
|
+
let full = '';
|
|
91
|
+
for await (const piece of openaiStream) {
|
|
92
|
+
full += piece.text;
|
|
93
|
+
throttle.push(piece.text, broadcastStream);
|
|
94
|
+
}
|
|
95
|
+
throttle.flush(broadcastStream);
|
|
96
|
+
|
|
97
|
+
return { status: 'success', message: full };
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Recipients consume both `broadcastStream` and `streamTo` chunks via the same `upsertSyncEventCallback` they already use:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
upsertSyncEventCallback({
|
|
105
|
+
name: 'chat/sendMessage',
|
|
106
|
+
version: 'v1',
|
|
107
|
+
callback: ({ stream, status }) => {
|
|
108
|
+
if (status === 'stream' && stream?.chunk) appendToken(stream.chunk);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Full decision tree, performance notes, and additional examples live in [`docs/ARCHITECTURE_SYNC.md`](../../docs/ARCHITECTURE_SYNC.md#streaming).
|
|
114
|
+
|
|
115
|
+
## Public API
|
|
116
|
+
|
|
117
|
+
Server entry (`@luckystack/sync`):
|
|
118
|
+
|
|
119
|
+
| Export | Purpose |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `handleSyncRequest(socket, msg, ack)` | Socket.io sync handler (default export). |
|
|
122
|
+
| `handleHttpSyncRequest(req, res)` | HTTP/SSE fallback. |
|
|
123
|
+
| `createStreamThrottle(options)` | Coalesce small stream pieces into bigger chunks (LLM-token-friendly). |
|
|
124
|
+
| Type: `HttpSyncStreamEvent` | SSE event shape. |
|
|
125
|
+
| Type: `StreamThrottle` / `CreateStreamThrottleOptions` | Throttle helper types. |
|
|
126
|
+
|
|
127
|
+
Configure stream throttling and offline-queue policy via `registerProjectConfig({ sync, offlineQueue })`. The shapes are exported from `@luckystack/core` as **`SyncConfig`** (with nested `SyncStreamThrottleConfig`) and **`OfflineQueueConfig`** — they cover the throttle defaults, fanout iteration tuning, and the queue's max-size + drop policy (`'reject'` triggers the `offline.queueFull` error code on overflow).
|
|
128
|
+
|
|
129
|
+
Client entry (`@luckystack/sync/client`):
|
|
130
|
+
|
|
131
|
+
| Export | Purpose |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `syncRequest(opts)` | Fire a typed sync event, optionally with `ignoreSelf` and `receiver`. |
|
|
134
|
+
| `upsertSyncEventCallback({ name, version, callback })` | Subscribe to inbound sync payloads. |
|
|
135
|
+
| `useSyncEvents(...)` | React hook for component-scoped subscriptions. |
|
|
136
|
+
|
|
137
|
+
## Related architecture docs
|
|
138
|
+
|
|
139
|
+
- [`docs/ARCHITECTURE_SYNC.md`](../../docs/ARCHITECTURE_SYNC.md) — full sync lifecycle, streaming decision tree, performance notes.
|
|
140
|
+
- [`docs/ARCHITECTURE_SOCKET.md`](../../docs/ARCHITECTURE_SOCKET.md) — Socket.io + Redis adapter (required for cross-instance fanout).
|
|
141
|
+
- [`docs/ARCHITECTURE_ROUTING.md`](../../docs/ARCHITECTURE_ROUTING.md) — `_sync/` file conventions and `_server` / `_client` split.
|
|
142
|
+
- [`docs/STREAMING_RECONSTRUCTION.md`](../../docs/STREAMING_RECONSTRUCTION.md) — recreating the streaming demo page.
|
|
143
|
+
|
|
144
|
+
## Dependencies
|
|
145
|
+
|
|
146
|
+
- Runtime: `@luckystack/core`, `@luckystack/login`, `@luckystack/error-tracking`
|
|
147
|
+
- Peer (canonical ranges, standardized 2026-05-07):
|
|
148
|
+
- `@prisma/client@^6.19.0` (transitively required via `@luckystack/core`)
|
|
149
|
+
- `react@^19.2.0` (`/client` entry only)
|
|
150
|
+
- `socket.io@^4.8.0`
|
|
151
|
+
- `socket.io-client@^4.8.0`
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT — see [LICENSE](../../LICENSE).
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { StreamPayload, statusContent, BaseSessionLayout, SyncTypeMap } from '@luckystack/core/client';
|
|
2
|
+
import { Dispatch, SetStateAction, RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
type SyncRequestStreamEvent<T extends StreamPayload = StreamPayload> = T;
|
|
5
|
+
type SyncRouteStreamEvent<T extends StreamPayload = StreamPayload> = T;
|
|
6
|
+
type DataRequired<T> = Record<string, never> extends T ? false : true;
|
|
7
|
+
type UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I : never;
|
|
8
|
+
type Prettify<T> = {
|
|
9
|
+
[K in keyof T]: T[K];
|
|
10
|
+
} & {};
|
|
11
|
+
type SyncRouteRecord = UnionToIntersection<{
|
|
12
|
+
[P in keyof SyncTypeMap]: {
|
|
13
|
+
[N in keyof SyncTypeMap[P] as P extends 'root' ? `system/${Extract<N, string>}` : `${Extract<P, string>}/${Extract<N, string>}`]: SyncTypeMap[P][N];
|
|
14
|
+
};
|
|
15
|
+
}[keyof SyncTypeMap]>;
|
|
16
|
+
type SyncFullName = Extract<keyof SyncRouteRecord, string>;
|
|
17
|
+
type VersionsForFullName<F extends SyncFullName> = Extract<keyof SyncRouteRecord[F], string>;
|
|
18
|
+
type ClientInputForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends {
|
|
19
|
+
clientInput: infer I;
|
|
20
|
+
} ? I : never;
|
|
21
|
+
type ServerOutputForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends {
|
|
22
|
+
serverOutput: infer O;
|
|
23
|
+
} ? O : never;
|
|
24
|
+
type ClientOutputForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends {
|
|
25
|
+
clientOutput: infer O;
|
|
26
|
+
} ? O : never;
|
|
27
|
+
type ServerStreamForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends {
|
|
28
|
+
serverStream: infer O;
|
|
29
|
+
} ? O : never;
|
|
30
|
+
type ClientStreamForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends {
|
|
31
|
+
clientStream: infer O;
|
|
32
|
+
} ? O : never;
|
|
33
|
+
type SyncRequestStreamCallbackForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = [
|
|
34
|
+
ServerStreamForFullName<F, V>
|
|
35
|
+
] extends [never] ? never : (event: SyncRequestStreamEvent<Prettify<ServerStreamForFullName<F, V> extends StreamPayload ? ServerStreamForFullName<F, V> : StreamPayload>>) => void;
|
|
36
|
+
type CombinedRouteStream<F extends SyncFullName, V extends VersionsForFullName<F>> = (ClientStreamForFullName<F, V> extends never ? never : ClientStreamForFullName<F, V>) | (ServerStreamForFullName<F, V> extends never ? never : ServerStreamForFullName<F, V>);
|
|
37
|
+
type SyncRouteStreamCallbackForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = [
|
|
38
|
+
CombinedRouteStream<F, V>
|
|
39
|
+
] extends [never] ? never : (params: {
|
|
40
|
+
stream: SyncRouteStreamEvent<Prettify<CombinedRouteStream<F, V> extends StreamPayload ? CombinedRouteStream<F, V> : StreamPayload>>;
|
|
41
|
+
}) => void;
|
|
42
|
+
type SyncParamsForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = DataRequired<ClientInputForFullName<F, V>> extends true ? {
|
|
43
|
+
name: F;
|
|
44
|
+
version: V;
|
|
45
|
+
data: ClientInputForFullName<F, V>;
|
|
46
|
+
receiver: string;
|
|
47
|
+
ignoreSelf?: boolean;
|
|
48
|
+
onStream?: SyncRequestStreamCallbackForFullName<F, V>;
|
|
49
|
+
/**
|
|
50
|
+
* Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a
|
|
51
|
+
* specific sync ("editor cursor move") pick `'drop-oldest'` while the
|
|
52
|
+
* app default stays `'reject'` for safer sends. When omitted, falls back
|
|
53
|
+
* to the global config.
|
|
54
|
+
*/
|
|
55
|
+
offlineDropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
|
|
56
|
+
/**
|
|
57
|
+
* Optional AbortSignal. When aborted the client emits `syncCancel { cb }`
|
|
58
|
+
* to the server and resolves locally with
|
|
59
|
+
* `{ status: 'error', errorCode: 'request.aborted' }`.
|
|
60
|
+
*/
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
} : {
|
|
63
|
+
name: F;
|
|
64
|
+
version: V;
|
|
65
|
+
data?: ClientInputForFullName<F, V>;
|
|
66
|
+
receiver: string;
|
|
67
|
+
ignoreSelf?: boolean;
|
|
68
|
+
onStream?: SyncRequestStreamCallbackForFullName<F, V>;
|
|
69
|
+
/** Per-request override (see typed branch). */
|
|
70
|
+
offlineDropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
|
|
71
|
+
/** Optional AbortSignal — see typed branch. */
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
};
|
|
74
|
+
interface SyncErrorParam {
|
|
75
|
+
key: string;
|
|
76
|
+
value: string | number | boolean;
|
|
77
|
+
}
|
|
78
|
+
interface SyncResponseError {
|
|
79
|
+
status: 'error';
|
|
80
|
+
message: string;
|
|
81
|
+
errorCode: string;
|
|
82
|
+
errorParams?: SyncErrorParam[];
|
|
83
|
+
httpStatus?: number;
|
|
84
|
+
}
|
|
85
|
+
type SyncResultForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = [
|
|
86
|
+
ServerOutputForFullName<F, V>
|
|
87
|
+
] extends [never] ? Record<string, never> : ServerOutputForFullName<F, V>;
|
|
88
|
+
type SyncRequestResponseForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncResponseError | {
|
|
89
|
+
status: 'success';
|
|
90
|
+
message: string;
|
|
91
|
+
result: SyncResultForFullName<F, V>;
|
|
92
|
+
};
|
|
93
|
+
type SyncRequestParamsWithOptions<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncParamsForFullName<F, V>;
|
|
94
|
+
declare function syncRequest<F extends SyncFullName, V extends VersionsForFullName<F>>(params: SyncRequestParamsWithOptions<F, V>): Promise<Prettify<SyncRequestResponseForFullName<F, V>>>;
|
|
95
|
+
interface TypedCallbackParams<F extends SyncFullName, V extends VersionsForFullName<F>> {
|
|
96
|
+
clientOutput: ClientOutputForFullName<F, V>;
|
|
97
|
+
serverOutput: ServerOutputForFullName<F, V>;
|
|
98
|
+
}
|
|
99
|
+
interface UpsertParams<F extends SyncFullName, V extends VersionsForFullName<F>> {
|
|
100
|
+
name: F;
|
|
101
|
+
version: V;
|
|
102
|
+
callback: (params: TypedCallbackParams<F, V>) => void;
|
|
103
|
+
}
|
|
104
|
+
interface UpsertStreamParams<F extends SyncFullName, V extends VersionsForFullName<F>> {
|
|
105
|
+
name: F;
|
|
106
|
+
version: V;
|
|
107
|
+
callback: SyncRouteStreamCallbackForFullName<F, V>;
|
|
108
|
+
}
|
|
109
|
+
declare const useSyncEvents: () => {
|
|
110
|
+
upsertSyncEventCallback: <F extends SyncFullName, V extends VersionsForFullName<F>>(params: UpsertParams<F, V>) => (() => void);
|
|
111
|
+
upsertSyncEventStreamCallback: <F extends SyncFullName, V extends VersionsForFullName<F>>(params: UpsertStreamParams<F, V>) => (() => void);
|
|
112
|
+
};
|
|
113
|
+
declare const useSyncEventTrigger: () => {
|
|
114
|
+
triggerSyncEvent: (name: string, clientOutput?: unknown, serverOutput?: unknown) => void;
|
|
115
|
+
triggerSyncStreamEvent: (name: string, stream: SyncRouteStreamEvent) => void;
|
|
116
|
+
};
|
|
117
|
+
type SocketStatusSetter = Dispatch<SetStateAction<{
|
|
118
|
+
self: statusContent;
|
|
119
|
+
[userId: string]: statusContent;
|
|
120
|
+
}>>;
|
|
121
|
+
declare const initSyncRequest: ({ setSocketStatus, sessionRef }: {
|
|
122
|
+
setSocketStatus: SocketStatusSetter;
|
|
123
|
+
sessionRef: RefObject<BaseSessionLayout | null> | null;
|
|
124
|
+
}) => Promise<void>;
|
|
125
|
+
|
|
126
|
+
export { type SyncRequestStreamEvent, type SyncRouteStreamEvent, initSyncRequest, syncRequest, useSyncEventTrigger, useSyncEvents };
|