@luckystack/sync 0.1.8 → 0.1.9
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 -14
- package/docs/callback-registration.md +257 -257
- package/docs/error-states.md +252 -252
- package/docs/ignore-self.md +162 -162
- package/docs/room-fanout.md +233 -233
- package/docs/server-vs-client-handlers.md +321 -321
- package/docs/sync-request.md +284 -284
- package/docs/version-policy.md +362 -362
- package/package.json +4 -4
package/docs/ignore-self.md
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
# ignore-self
|
|
2
|
-
|
|
3
|
-
> `ignoreSelf: true` on `syncRequest` tells `handleSyncRequest` to skip every recipient socket whose session token matches the sender's. The check is **token-based, not socket-id-based** — multi-tab users skip ALL of their own tabs. The originator's ack response still arrives.
|
|
4
|
-
|
|
5
|
-
For the full `syncRequest` signature see [`./sync-request.md`](./sync-request.md). For the fanout loop's surrounding mechanics see [`./room-fanout.md`](./room-fanout.md).
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. What the flag actually does
|
|
10
|
-
|
|
11
|
-
The originator passes `ignoreSelf: true`. Inside the per-recipient loop:
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
const tempToken = extractTokenFromSocket(tempSocket);
|
|
15
|
-
|
|
16
|
-
if (ignoreSelf && typeof ignoreSelf === 'boolean' && token === tempToken) {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
recipientCount++;
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
`token` is the sender's session token (resolved from their socket at the top of `handleSyncRequest`). `tempToken` is the recipient's session token (extracted from THAT socket via `extractTokenFromSocket`).
|
|
24
|
-
|
|
25
|
-
When the two match:
|
|
26
|
-
|
|
27
|
-
- That recipient is **not** emitted to.
|
|
28
|
-
- That recipient does **not** count toward `recipientCount` (so `postSyncFanout` sees a number that reflects actual sends).
|
|
29
|
-
- The fanout loop continues immediately to the next socket.
|
|
30
|
-
|
|
31
|
-
The originator still receives:
|
|
32
|
-
|
|
33
|
-
- The normal ack via `buildSyncResponseEventName(responseIndex)` (the success/error envelope with `serverOutput`).
|
|
34
|
-
- Any chunks emitted via the originator-targeted `stream(payload)` (those flow on the originator's unicast progress channel, not the broadcast room).
|
|
35
|
-
|
|
36
|
-
What the originator does NOT receive when `ignoreSelf: true`:
|
|
37
|
-
|
|
38
|
-
- The room frame (the `{ serverOutput, clientOutput, ... status: 'success' }` payload that `upsertSyncEventCallback` consumes).
|
|
39
|
-
- `broadcastStream` and `streamTo` chunks targeted at their own token room.
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## 2. When to use `ignoreSelf: true`
|
|
44
|
-
|
|
45
|
-
The canonical case: **optimistic UI**. The sender already applied the local change before calling `syncRequest`, so receiving the broadcast back just causes a no-op re-render (and risks visual flicker if the local optimistic state and the broadcast payload differ subtly).
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
// Local update (optimistic):
|
|
49
|
-
setCards(prev => moveCardLocally(prev, cardId, toLane));
|
|
50
|
-
|
|
51
|
-
// Tell everyone else:
|
|
52
|
-
await syncRequest({
|
|
53
|
-
name: 'board/moveCard',
|
|
54
|
-
version: 'v1',
|
|
55
|
-
data: { cardId, toLane },
|
|
56
|
-
receiver: roomCode,
|
|
57
|
-
ignoreSelf: true,
|
|
58
|
-
});
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Other use cases:
|
|
62
|
-
|
|
63
|
-
- **Drift-tolerant counters / cursor positions / typing indicators** — the sender's state is authoritative locally; rebroadcasting to them is pure waste.
|
|
64
|
-
- **Chat message sends** — the sender already rendered "you said …" in the input box; the broadcast is for everyone else.
|
|
65
|
-
- **Acknowledgment-pattern sends** — the sender awaits the ack to know it persisted; they don't need a separate "your message arrived" frame.
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## 3. When to leave `ignoreSelf: false` (or omit it)
|
|
70
|
-
|
|
71
|
-
Default to `false` (or just don't set it) when **the sender needs to receive the server's authoritative version** to reconcile their optimistic guess.
|
|
72
|
-
|
|
73
|
-
- **Server-assigned IDs** — the optimistic version had a temp UUID, the broadcast payload has the real DB primary key. The sender needs to listen.
|
|
74
|
-
- **Server-computed derived fields** — timestamps (`createdAt`), running totals (`balance`), search scores, anything the client could not have known.
|
|
75
|
-
- **Cross-validation refresh** — the sender wants to confirm "yes, my mutation actually landed in the shape I expected" via the broadcast.
|
|
76
|
-
- **Recipients are different users** — when the sender is NOT a member of the room they're sending to (e.g. an admin pushing an announcement), `ignoreSelf` is irrelevant. Leave it false.
|
|
77
|
-
|
|
78
|
-
If you're unsure, leave it `false`. Receiving an extra broadcast frame is cheap; **missing one is expensive** because you can't easily detect the absence.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## 4. Edge case: multiple sockets per user (tabs, devices)
|
|
83
|
-
|
|
84
|
-
`ignoreSelf` compares **session tokens**, not socket IDs. If a user has 3 tabs open:
|
|
85
|
-
|
|
86
|
-
- Tab A initiates `syncRequest({ ignoreSelf: true })`.
|
|
87
|
-
- Tabs A, B, and C are all in the room (they all auto-joined the `<sessionToken>` room AND any shared room they're members of).
|
|
88
|
-
- All three tabs share the same session token.
|
|
89
|
-
|
|
90
|
-
Result: **all three tabs skip the broadcast**, not just Tab A.
|
|
91
|
-
|
|
92
|
-
This is usually what you want — when you optimistically update a piece of state in any one tab, the framework's `SessionProvider` typically broadcasts the change to all tabs of the same user via the per-token room separately. But it's worth being aware of: `ignoreSelf` is a per-user filter, not a per-socket filter.
|
|
93
|
-
|
|
94
|
-
If you ever genuinely want "skip Tab A but notify Tabs B and C", you have to model that explicitly — either with separate logical session tokens per tab (atypical) or by leaving `ignoreSelf: false` and de-duplicating client-side.
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## 5. Interaction with `recipientCount`
|
|
99
|
-
|
|
100
|
-
`recipientCount` is what `postSyncFanout` reports. Skipped recipients (whether via `ignoreSelf` or because the socket disappeared mid-fanout) are NOT counted.
|
|
101
|
-
|
|
102
|
-
Sample timeline:
|
|
103
|
-
|
|
104
|
-
- Room has 5 sockets, 3 of which belong to the sender's session token.
|
|
105
|
-
- `syncRequest({ ignoreSelf: true })` from one of the sender's sockets.
|
|
106
|
-
- Fanout skips all 3 of the sender's sockets.
|
|
107
|
-
- `recipientCount === 2`.
|
|
108
|
-
- `postSyncFanout` payload: `{ ..., recipientCount: 2 }`.
|
|
109
|
-
|
|
110
|
-
This makes metrics observe "actual sends", not "room size" — useful for "how many viewers did this update actually reach".
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## 6. Interaction with streaming
|
|
115
|
-
|
|
116
|
-
`broadcastStream` and `streamTo` happen **inside** `_server`'s `main()`, BEFORE the fanout loop reaches the skip step. The skip is per-final-emit only.
|
|
117
|
-
|
|
118
|
-
Implication: a `broadcastStream` call sends chunks to **every socket in the room INCLUDING the sender's own sockets**, regardless of whether the final ack-step fanout will skip them.
|
|
119
|
-
|
|
120
|
-
If you need to exclude the sender from broadcast streaming too, use `streamTo` with an explicit list that excludes the sender's token:
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
// inside _server_v1.ts main()
|
|
124
|
-
const others = (await functions.session.getRoomMembers(roomCode)).filter(t => t !== user.id);
|
|
125
|
-
streamTo(others, { chunk: piece });
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
This isn't a common need — typically the sender genuinely does want to see their own LLM tokens stream because they initiated the conversation. But it's available.
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## 7. Why the originator still gets the ack with `ignoreSelf: true`
|
|
133
|
-
|
|
134
|
-
The ack response (`buildSyncResponseEventName(responseIndex)`) is the protocol-level "your request succeeded / failed" envelope. It is **never** a fanout payload — it's a direct unicast `socket.emit` to the originator's responseIndex channel, and it carries `serverOutput` so the originator can reconcile their optimistic state with the server's authoritative version.
|
|
135
|
-
|
|
136
|
-
`ignoreSelf` only affects the **room fanout step**. The ack is wired through a separate channel that bypasses the fanout entirely.
|
|
137
|
-
|
|
138
|
-
This is critical: the originator's `syncRequest` `await` would never resolve if `ignoreSelf` suppressed the ack. The split between "broadcast payload" and "originator ack" is what makes the optimistic-UI pattern usable.
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
## 8. Quick reference
|
|
143
|
-
|
|
144
|
-
| Behavior with `ignoreSelf: true` | |
|
|
145
|
-
|---|---|
|
|
146
|
-
| Sender's tabs skip the broadcast frame | yes |
|
|
147
|
-
| Sender receives ack with `serverOutput` | yes |
|
|
148
|
-
| Sender receives originator-targeted `stream(...)` chunks | yes |
|
|
149
|
-
| Sender receives `broadcastStream(...)` chunks | **yes — broadcastStream runs in `_server` before skip** |
|
|
150
|
-
| Sender receives `streamTo(...)` chunks (when token included) | **yes — same reason** |
|
|
151
|
-
| Other tabs of same user skipped too | yes — token match, not socket-id match |
|
|
152
|
-
| `recipientCount` counts skipped sockets | no |
|
|
153
|
-
| Same flag works on HTTP transport | yes — `handleHttpSyncRequest` has the same skip block |
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
## 9. Related
|
|
158
|
-
|
|
159
|
-
- Originator API: [`./sync-request.md`](./sync-request.md)
|
|
160
|
-
- Fanout mechanics: [`./room-fanout.md`](./room-fanout.md)
|
|
161
|
-
- Stream audience matrix: [`./streaming.md`](./streaming.md)
|
|
162
|
-
- Server handler params (`user.id` vs sender's token): [`./server-vs-client-handlers.md`](./server-vs-client-handlers.md)
|
|
1
|
+
# ignore-self
|
|
2
|
+
|
|
3
|
+
> `ignoreSelf: true` on `syncRequest` tells `handleSyncRequest` to skip every recipient socket whose session token matches the sender's. The check is **token-based, not socket-id-based** — multi-tab users skip ALL of their own tabs. The originator's ack response still arrives.
|
|
4
|
+
|
|
5
|
+
For the full `syncRequest` signature see [`./sync-request.md`](./sync-request.md). For the fanout loop's surrounding mechanics see [`./room-fanout.md`](./room-fanout.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. What the flag actually does
|
|
10
|
+
|
|
11
|
+
The originator passes `ignoreSelf: true`. Inside the per-recipient loop:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const tempToken = extractTokenFromSocket(tempSocket);
|
|
15
|
+
|
|
16
|
+
if (ignoreSelf && typeof ignoreSelf === 'boolean' && token === tempToken) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
recipientCount++;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`token` is the sender's session token (resolved from their socket at the top of `handleSyncRequest`). `tempToken` is the recipient's session token (extracted from THAT socket via `extractTokenFromSocket`).
|
|
24
|
+
|
|
25
|
+
When the two match:
|
|
26
|
+
|
|
27
|
+
- That recipient is **not** emitted to.
|
|
28
|
+
- That recipient does **not** count toward `recipientCount` (so `postSyncFanout` sees a number that reflects actual sends).
|
|
29
|
+
- The fanout loop continues immediately to the next socket.
|
|
30
|
+
|
|
31
|
+
The originator still receives:
|
|
32
|
+
|
|
33
|
+
- The normal ack via `buildSyncResponseEventName(responseIndex)` (the success/error envelope with `serverOutput`).
|
|
34
|
+
- Any chunks emitted via the originator-targeted `stream(payload)` (those flow on the originator's unicast progress channel, not the broadcast room).
|
|
35
|
+
|
|
36
|
+
What the originator does NOT receive when `ignoreSelf: true`:
|
|
37
|
+
|
|
38
|
+
- The room frame (the `{ serverOutput, clientOutput, ... status: 'success' }` payload that `upsertSyncEventCallback` consumes).
|
|
39
|
+
- `broadcastStream` and `streamTo` chunks targeted at their own token room.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. When to use `ignoreSelf: true`
|
|
44
|
+
|
|
45
|
+
The canonical case: **optimistic UI**. The sender already applied the local change before calling `syncRequest`, so receiving the broadcast back just causes a no-op re-render (and risks visual flicker if the local optimistic state and the broadcast payload differ subtly).
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// Local update (optimistic):
|
|
49
|
+
setCards(prev => moveCardLocally(prev, cardId, toLane));
|
|
50
|
+
|
|
51
|
+
// Tell everyone else:
|
|
52
|
+
await syncRequest({
|
|
53
|
+
name: 'board/moveCard',
|
|
54
|
+
version: 'v1',
|
|
55
|
+
data: { cardId, toLane },
|
|
56
|
+
receiver: roomCode,
|
|
57
|
+
ignoreSelf: true,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Other use cases:
|
|
62
|
+
|
|
63
|
+
- **Drift-tolerant counters / cursor positions / typing indicators** — the sender's state is authoritative locally; rebroadcasting to them is pure waste.
|
|
64
|
+
- **Chat message sends** — the sender already rendered "you said …" in the input box; the broadcast is for everyone else.
|
|
65
|
+
- **Acknowledgment-pattern sends** — the sender awaits the ack to know it persisted; they don't need a separate "your message arrived" frame.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 3. When to leave `ignoreSelf: false` (or omit it)
|
|
70
|
+
|
|
71
|
+
Default to `false` (or just don't set it) when **the sender needs to receive the server's authoritative version** to reconcile their optimistic guess.
|
|
72
|
+
|
|
73
|
+
- **Server-assigned IDs** — the optimistic version had a temp UUID, the broadcast payload has the real DB primary key. The sender needs to listen.
|
|
74
|
+
- **Server-computed derived fields** — timestamps (`createdAt`), running totals (`balance`), search scores, anything the client could not have known.
|
|
75
|
+
- **Cross-validation refresh** — the sender wants to confirm "yes, my mutation actually landed in the shape I expected" via the broadcast.
|
|
76
|
+
- **Recipients are different users** — when the sender is NOT a member of the room they're sending to (e.g. an admin pushing an announcement), `ignoreSelf` is irrelevant. Leave it false.
|
|
77
|
+
|
|
78
|
+
If you're unsure, leave it `false`. Receiving an extra broadcast frame is cheap; **missing one is expensive** because you can't easily detect the absence.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 4. Edge case: multiple sockets per user (tabs, devices)
|
|
83
|
+
|
|
84
|
+
`ignoreSelf` compares **session tokens**, not socket IDs. If a user has 3 tabs open:
|
|
85
|
+
|
|
86
|
+
- Tab A initiates `syncRequest({ ignoreSelf: true })`.
|
|
87
|
+
- Tabs A, B, and C are all in the room (they all auto-joined the `<sessionToken>` room AND any shared room they're members of).
|
|
88
|
+
- All three tabs share the same session token.
|
|
89
|
+
|
|
90
|
+
Result: **all three tabs skip the broadcast**, not just Tab A.
|
|
91
|
+
|
|
92
|
+
This is usually what you want — when you optimistically update a piece of state in any one tab, the framework's `SessionProvider` typically broadcasts the change to all tabs of the same user via the per-token room separately. But it's worth being aware of: `ignoreSelf` is a per-user filter, not a per-socket filter.
|
|
93
|
+
|
|
94
|
+
If you ever genuinely want "skip Tab A but notify Tabs B and C", you have to model that explicitly — either with separate logical session tokens per tab (atypical) or by leaving `ignoreSelf: false` and de-duplicating client-side.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 5. Interaction with `recipientCount`
|
|
99
|
+
|
|
100
|
+
`recipientCount` is what `postSyncFanout` reports. Skipped recipients (whether via `ignoreSelf` or because the socket disappeared mid-fanout) are NOT counted.
|
|
101
|
+
|
|
102
|
+
Sample timeline:
|
|
103
|
+
|
|
104
|
+
- Room has 5 sockets, 3 of which belong to the sender's session token.
|
|
105
|
+
- `syncRequest({ ignoreSelf: true })` from one of the sender's sockets.
|
|
106
|
+
- Fanout skips all 3 of the sender's sockets.
|
|
107
|
+
- `recipientCount === 2`.
|
|
108
|
+
- `postSyncFanout` payload: `{ ..., recipientCount: 2 }`.
|
|
109
|
+
|
|
110
|
+
This makes metrics observe "actual sends", not "room size" — useful for "how many viewers did this update actually reach".
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 6. Interaction with streaming
|
|
115
|
+
|
|
116
|
+
`broadcastStream` and `streamTo` happen **inside** `_server`'s `main()`, BEFORE the fanout loop reaches the skip step. The skip is per-final-emit only.
|
|
117
|
+
|
|
118
|
+
Implication: a `broadcastStream` call sends chunks to **every socket in the room INCLUDING the sender's own sockets**, regardless of whether the final ack-step fanout will skip them.
|
|
119
|
+
|
|
120
|
+
If you need to exclude the sender from broadcast streaming too, use `streamTo` with an explicit list that excludes the sender's token:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// inside _server_v1.ts main()
|
|
124
|
+
const others = (await functions.session.getRoomMembers(roomCode)).filter(t => t !== user.id);
|
|
125
|
+
streamTo(others, { chunk: piece });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This isn't a common need — typically the sender genuinely does want to see their own LLM tokens stream because they initiated the conversation. But it's available.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 7. Why the originator still gets the ack with `ignoreSelf: true`
|
|
133
|
+
|
|
134
|
+
The ack response (`buildSyncResponseEventName(responseIndex)`) is the protocol-level "your request succeeded / failed" envelope. It is **never** a fanout payload — it's a direct unicast `socket.emit` to the originator's responseIndex channel, and it carries `serverOutput` so the originator can reconcile their optimistic state with the server's authoritative version.
|
|
135
|
+
|
|
136
|
+
`ignoreSelf` only affects the **room fanout step**. The ack is wired through a separate channel that bypasses the fanout entirely.
|
|
137
|
+
|
|
138
|
+
This is critical: the originator's `syncRequest` `await` would never resolve if `ignoreSelf` suppressed the ack. The split between "broadcast payload" and "originator ack" is what makes the optimistic-UI pattern usable.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 8. Quick reference
|
|
143
|
+
|
|
144
|
+
| Behavior with `ignoreSelf: true` | |
|
|
145
|
+
|---|---|
|
|
146
|
+
| Sender's tabs skip the broadcast frame | yes |
|
|
147
|
+
| Sender receives ack with `serverOutput` | yes |
|
|
148
|
+
| Sender receives originator-targeted `stream(...)` chunks | yes |
|
|
149
|
+
| Sender receives `broadcastStream(...)` chunks | **yes — broadcastStream runs in `_server` before skip** |
|
|
150
|
+
| Sender receives `streamTo(...)` chunks (when token included) | **yes — same reason** |
|
|
151
|
+
| Other tabs of same user skipped too | yes — token match, not socket-id match |
|
|
152
|
+
| `recipientCount` counts skipped sockets | no |
|
|
153
|
+
| Same flag works on HTTP transport | yes — `handleHttpSyncRequest` has the same skip block |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 9. Related
|
|
158
|
+
|
|
159
|
+
- Originator API: [`./sync-request.md`](./sync-request.md)
|
|
160
|
+
- Fanout mechanics: [`./room-fanout.md`](./room-fanout.md)
|
|
161
|
+
- Stream audience matrix: [`./streaming.md`](./streaming.md)
|
|
162
|
+
- Server handler params (`user.id` vs sender's token): [`./server-vs-client-handlers.md`](./server-vs-client-handlers.md)
|