Skip to content

Commit feaf55e

Browse files
authored
Merge pull request #131 from ether/fix/insert-author-and-trailing-newline
fix: send author-attributed inserts and preserve trailing newline
2 parents 592fa8c + b0f95c9 commit feaf55e

7 files changed

Lines changed: 54 additions & 138 deletions

File tree

.github/workflows/backend-tests.yml

Lines changed: 0 additions & 75 deletions
This file was deleted.

.github/workflows/npmpublish.yml

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,57 +10,35 @@ on:
1010
- main
1111
- master
1212

13+
env:
14+
PNPM_HOME: ~/.pnpm-store
15+
1316
jobs:
1417
test:
1518
runs-on: ubuntu-latest
1619
steps:
1720
-
1821
uses: actions/checkout@v6
19-
with:
20-
repository: ether/etherpad-lite
21-
path: etherpad-lite
22-
-
23-
run: mv etherpad-lite ..
24-
-
25-
uses: actions/checkout@v6
26-
-
27-
uses: actions/setup-node@v4
28-
with:
29-
node-version: 20
30-
- uses: pnpm/action-setup@v4
31-
name: Install pnpm
32-
with:
33-
version: 10
34-
run_install: false
35-
- name: Get pnpm store directory
36-
shell: bash
37-
run: |
38-
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
3922
- uses: actions/cache@v5
40-
name: Setup pnpm cache
23+
name: Cache pnpm store
4124
with:
42-
path: ${{ env.STORE_PATH }}
25+
path: ${{ env.PNPM_HOME }}
4326
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
4427
restore-keys: |
4528
${{ runner.os }}-pnpm-store-
29+
- uses: pnpm/action-setup@v6
30+
name: Install pnpm
31+
with:
32+
run_install: false
4633
-
47-
run: cd ../etherpad-lite && ./bin/installDeps.sh && pnpm link --global
34+
uses: actions/setup-node@v6
35+
with:
36+
node-version: 24
37+
cache: pnpm
4838
-
49-
run: |
50-
pnpm config set auto-install-peers false
51-
pnpm i
39+
run: pnpm i
5240
-
53-
run: |
54-
has_testcli_script () {
55-
[[ $(pnpm run | grep "^ test" | wc -l) > 0 ]]
56-
}
57-
58-
if has_testcli_script; then
59-
pnpm run test
60-
else
61-
echo "No test script found"
62-
fi
63-
name: Run tests if available
41+
run: pnpm run build
6442
-
6543
run: pnpm run lint
6644

@@ -73,29 +51,23 @@ jobs:
7351
uses: actions/checkout@v6
7452
with:
7553
fetch-depth: 0
76-
-
77-
uses: actions/setup-node@v4
78-
with:
79-
node-version: 20
80-
registry-url: https://registry.npmjs.org/
81-
- uses: pnpm/action-setup@v4
82-
name: Install pnpm
83-
with:
84-
version: 10
85-
run_install: false
86-
- name: Get pnpm store directory
87-
shell: bash
88-
run: |
89-
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
9054
- uses: actions/cache@v5
91-
name: Setup pnpm cache
55+
name: Cache pnpm store
9256
with:
93-
path: ${{ env.STORE_PATH }}
57+
path: ${{ env.PNPM_HOME }}
9458
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
9559
restore-keys: |
9660
${{ runner.os }}-pnpm-store-
97-
- name: Only install direct dependencies
98-
run: pnpm config set auto-install-peers false
61+
- uses: pnpm/action-setup@v6
62+
name: Install pnpm
63+
with:
64+
run_install: false
65+
-
66+
uses: actions/setup-node@v6
67+
with:
68+
node-version: 24
69+
cache: pnpm
70+
registry-url: https://registry.npmjs.org/
9971
-
10072
name: Bump version (patch)
10173
run: |

.npmrc

Lines changed: 0 additions & 1 deletion
This file was deleted.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"name": "etherpad-cli-client",
33
"description": "Node Client for Etherpad",
4-
"version": "4.0.2",
4+
"version": "4.0.3",
55
"type": "module",
6+
"packageManager": "pnpm@11.1.2",
67
"author": {
78
"name": "John McLear",
89
"email": "john@mclear.co.uk",

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
onlyBuiltDependencies:
22
- unrs-resolver
3+
strictDepBuilds: false

src/index.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type PadClient = EventEmitter & {
3838
type ClientVarsMessage = {
3939
type: 'CLIENT_VARS';
4040
data: {
41+
userId?: string;
4142
collab_client_vars: {
4243
initialAttributedText: AText;
4344
apool: JsonableAttributePool;
@@ -108,6 +109,7 @@ const isDisconnectMessage = (value: unknown): value is DisconnectMessage =>
108109

109110
export const connect = (host?: string): PadClient => {
110111
const ee = new EventEmitter() as PadClient;
112+
let authorId: string | null = null;
111113
const padState: PadState = {
112114
host: '',
113115
path: '',
@@ -198,6 +200,7 @@ export const connect = (host?: string): PadClient => {
198200
padState.atext = obj.data.collab_client_vars.initialAttributedText;
199201
padState.apool = new AttributePool().fromJsonable(obj.data.collab_client_vars.apool);
200202
padState.baseRev = obj.data.collab_client_vars.rev;
203+
if (typeof obj.data.userId === 'string') authorId = obj.data.userId;
201204
ee.emit('connected', padState);
202205
} else if (isNewChangesMessage(obj)) {
203206
if (obj.data.newRev <= padState.baseRev) return;
@@ -240,16 +243,31 @@ export const connect = (host?: string): PadClient => {
240243
};
241244

242245
ee.append = (text: string) => {
243-
const newChangeset = Changeset.makeSplice(
244-
padState.atext.text, padState.atext.text.length, 0, text);
246+
// Insert just before the trailing '\n' so the pad's "doc always ends
247+
// with \n" invariant is preserved. Etherpad's server (post-2.7.x)
248+
// rejects USER_CHANGES whose application would leave the doc without
249+
// a trailing newline, and tags inserts with no `author` attribute as
250+
// bad changesets — both produced silent disconnects with the previous
251+
// append-at-text.length / no-attribs behaviour.
252+
const insertPos = Math.max(0, padState.atext.text.length - 1);
253+
const attribs: Array<[string, string]> | undefined =
254+
authorId ? [['author', authorId]] : undefined;
255+
const localChangeset = Changeset.makeSplice(
256+
padState.atext.text, insertPos, 0, text, attribs, padState.apool);
245257
const newRev = padState.baseRev;
246-
padState.atext = Changeset.applyToAText(newChangeset, padState.atext, padState.apool) as AText;
258+
padState.atext = Changeset.applyToAText(
259+
localChangeset, padState.atext, padState.apool) as AText;
260+
// Build a minimal wire pool containing only the attributes referenced
261+
// by this changeset so the server can resolve our `*N` slot numbers.
262+
const wireApool = new AttributePool();
263+
const wireChangeset = Changeset.moveOpsToNewPool(
264+
localChangeset, padState.apool, wireApool);
247265
const msg: PendingMessage = {
248266
component: 'pad',
249267
type: 'USER_CHANGES',
250268
baseRev: newRev,
251-
changeset: newChangeset,
252-
apool: new AttributePool().toJsonable(),
269+
changeset: wireChangeset,
270+
apool: wireApool.toJsonable(),
253271
};
254272
sendMessage(msg);
255273
};

0 commit comments

Comments
 (0)