ooxml-core 0.14.0 → 0.15.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/dist/automation/node.d.ts.map +1 -1
- package/dist/collab/broadcast-transport.d.ts.map +1 -1
- package/dist/collab/emitter.d.ts.map +1 -1
- package/dist/collab/index.cjs +3 -2
- package/dist/collab/index.mjs +3 -2
- package/dist/collab/ordering.d.ts.map +1 -1
- package/dist/crypto/detect.d.ts.map +1 -1
- package/dist/crypto/errors.d.ts +3 -3
- package/dist/crypto/errors.d.ts.map +1 -1
- package/dist/crypto/index.cjs +6 -6
- package/dist/crypto/index.mjs +6 -6
- package/dist/crypto/key-derivation.d.ts.map +1 -1
- package/dist/diagram/drawing.d.ts.map +1 -1
- package/dist/diagram/relationships.d.ts.map +1 -1
- package/dist/digest/sha512.d.ts.map +1 -1
- package/dist/docx/diagram.d.ts.map +1 -1
- package/dist/docx/docpr-ids.d.ts.map +1 -1
- package/dist/docx/field-runs.d.ts.map +1 -1
- package/dist/docx/generated/wml-simple-types.d.ts +99 -99
- package/dist/docx/generated/wml-simple-types.d.ts.map +1 -1
- package/dist/docx/heading-list-kinds.d.ts.map +1 -1
- package/dist/docx/highlight.d.ts +1 -1
- package/dist/docx/highlight.d.ts.map +1 -1
- package/dist/docx/layout/page-cursor.d.ts.map +1 -1
- package/dist/docx/ligatures.d.ts +1 -1
- package/dist/docx/ligatures.d.ts.map +1 -1
- package/dist/docx/load/index.cjs +18 -18
- package/dist/docx/load/index.mjs +18 -18
- package/dist/docx/load/legacy-doc.d.ts.map +1 -1
- package/dist/docx/paragraph-styles.d.ts +1 -1
- package/dist/docx/paragraph-styles.d.ts.map +1 -1
- package/dist/docx/relationship-allocator.d.ts.map +1 -1
- package/dist/docx/revision-commands.d.ts.map +1 -1
- package/dist/docx/simple-types.d.ts.map +1 -1
- package/dist/docx/table-of-contents.d.ts.map +1 -1
- package/dist/docx/table-of-figures.d.ts.map +1 -1
- package/dist/docx/test-support/restart-fixture.d.ts +3 -3
- package/dist/docx/test-support/restart-fixture.d.ts.map +1 -1
- package/dist/docx/validate-issues.d.ts.map +1 -1
- package/dist/docx/write-revisions.d.ts.map +1 -1
- package/dist/docx/xml.d.ts.map +1 -1
- package/dist/docx/zip-parts.d.ts.map +1 -1
- package/dist/opc/properties/types.d.ts +6 -6
- package/dist/opc/properties/types.d.ts.map +1 -1
- package/dist/opc/relationship-types.d.ts +6 -6
- package/dist/opc/relationship-types.d.ts.map +1 -1
- package/dist/opc/relationships.d.ts.map +1 -1
- package/dist/pptx/automation/index.cjs +138 -138
- package/dist/pptx/automation/index.d.ts +3 -3
- package/dist/pptx/automation/index.mjs +138 -138
- package/dist/pptx/chunks/{index-BWRd8p4y.d.ts → index-BtBj3VG1.d.ts} +3 -3
- package/dist/pptx/chunks/index-BtBj3VG1.d.ts.map +1 -0
- package/dist/pptx/chunks/{index-CT9y69AZ.d.ts → index-D0yGoLyw.d.ts} +136 -136
- package/dist/pptx/chunks/index-D0yGoLyw.d.ts.map +1 -0
- package/dist/pptx/chunks/{text-operations-2tKzrY5o.d.ts → text-operations-GuisjlbR.d.ts} +2 -2
- package/dist/pptx/chunks/{text-operations-2tKzrY5o.d.ts.map → text-operations-GuisjlbR.d.ts.map} +1 -1
- package/dist/pptx/cli/index.cjs +104 -104
- package/dist/pptx/cli/index.d.ts +1 -1
- package/dist/pptx/cli/index.mjs +104 -104
- package/dist/pptx/converter/index.d.ts +1 -1
- package/dist/pptx/index.cjs +138 -138
- package/dist/pptx/index.d.ts +3 -3
- package/dist/pptx/index.mjs +138 -138
- package/dist/teams/call.d.ts +61 -0
- package/dist/teams/call.d.ts.map +1 -0
- package/dist/teams/chat.d.ts +34 -0
- package/dist/teams/chat.d.ts.map +1 -0
- package/dist/teams/index.cjs +2207 -0
- package/dist/teams/index.d.ts +10 -0
- package/dist/teams/index.d.ts.map +1 -0
- package/dist/teams/index.mjs +2153 -0
- package/dist/teams/model.d.ts +49 -0
- package/dist/teams/model.d.ts.map +1 -0
- package/dist/teams/peer.d.ts +76 -0
- package/dist/teams/peer.d.ts.map +1 -0
- package/dist/teams/server-config.d.ts +27 -0
- package/dist/teams/server-config.d.ts.map +1 -0
- package/dist/teams/signaling.d.ts +81 -0
- package/dist/teams/signaling.d.ts.map +1 -0
- package/dist/teams/store.d.ts +113 -0
- package/dist/teams/store.d.ts.map +1 -0
- package/dist/teams/view.d.ts +77 -0
- package/dist/teams/view.d.ts.map +1 -0
- package/dist/teams/workspace.d.ts +51 -0
- package/dist/teams/workspace.d.ts.map +1 -0
- package/dist/units/units.d.ts.map +1 -1
- package/dist/visio/edit-geometry-admission.d.ts.map +1 -1
- package/dist/visio/edit-recalculate-index.d.ts.map +1 -1
- package/dist/visio/emf-admission-context.d.ts +1 -1
- package/dist/visio/emf-admission-context.d.ts.map +1 -1
- package/dist/visio/foreign-vector-types.d.ts.map +1 -1
- package/dist/visio/formula-master.d.ts +2 -2
- package/dist/visio/formula.d.ts.map +1 -1
- package/dist/visio/index.cjs +2048 -290
- package/dist/visio/index.mjs +2048 -290
- package/dist/visio/media.d.ts.map +1 -1
- package/dist/visio/package-common.d.ts.map +1 -1
- package/dist/visio/package.d.ts.map +1 -1
- package/dist/visio/sheet.d.ts.map +1 -1
- package/dist/visio/theme-color.d.ts.map +1 -1
- package/dist/xlsx/address.d.ts.map +1 -1
- package/dist/xlsx/cell-styles.d.ts.map +1 -1
- package/dist/xlsx/cells.d.ts.map +1 -1
- package/dist/xlsx/edit/calc-mode.d.ts.map +1 -1
- package/dist/xlsx/edit/clipboard-html.d.ts.map +1 -1
- package/dist/xlsx/edit/columns.d.ts.map +1 -1
- package/dist/xlsx/edit/context.d.ts.map +1 -1
- package/dist/xlsx/edit/fill-series.d.ts.map +1 -1
- package/dist/xlsx/edit/history.d.ts.map +1 -1
- package/dist/xlsx/edit/outline.d.ts.map +1 -1
- package/dist/xlsx/edit/range-math.d.ts.map +1 -1
- package/dist/xlsx/edit/session-calc.d.ts.map +1 -1
- package/dist/xlsx/edit/structure.d.ts.map +1 -1
- package/dist/xlsx/formula/area-index.d.ts.map +1 -1
- package/dist/xlsx/formula/ast.d.ts.map +1 -1
- package/dist/xlsx/formula/date-serial.d.ts.map +1 -1
- package/dist/xlsx/formula/engine-core.d.ts.map +1 -1
- package/dist/xlsx/formula/engine-host.d.ts.map +1 -1
- package/dist/xlsx/formula/engine-util.d.ts.map +1 -1
- package/dist/xlsx/formula/functions/criteria.d.ts.map +1 -1
- package/dist/xlsx/formula/functions/dist-core.d.ts.map +1 -1
- package/dist/xlsx/formula/functions/helpers.d.ts.map +1 -1
- package/dist/xlsx/formula/functions/math-combin.d.ts.map +1 -1
- package/dist/xlsx/formula/functions/registry.d.ts.map +1 -1
- package/dist/xlsx/formula/functions/stats-core.d.ts.map +1 -1
- package/dist/xlsx/formula/functions/text-case.d.ts.map +1 -1
- package/dist/xlsx/formula/functions/types.d.ts.map +1 -1
- package/dist/xlsx/formula/graph-index.d.ts.map +1 -1
- package/dist/xlsx/formula/lexemes.d.ts +1 -1
- package/dist/xlsx/formula/lexemes.d.ts.map +1 -1
- package/dist/xlsx/formula/references.d.ts.map +1 -1
- package/dist/xlsx/formula/spill.d.ts.map +1 -1
- package/dist/xlsx/formula/test-helpers.d.ts.map +1 -1
- package/dist/xlsx/formula/tokenizer.d.ts.map +1 -1
- package/dist/xlsx/formula/values.d.ts +2 -1
- package/dist/xlsx/formula/values.d.ts.map +1 -1
- package/dist/xlsx/layout/axis-metrics.d.ts.map +1 -1
- package/dist/xlsx/layout/chart-svg-util.d.ts.map +1 -1
- package/dist/xlsx/layout/colors.d.ts +1 -1
- package/dist/xlsx/layout/colors.d.ts.map +1 -1
- package/dist/xlsx/layout/tint.d.ts.map +1 -1
- package/dist/xlsx/layout/units.d.ts.map +1 -1
- package/dist/xlsx/load/errors.d.ts.map +1 -1
- package/dist/xlsx/load/index.cjs +24 -24
- package/dist/xlsx/load/index.mjs +24 -24
- package/dist/xlsx/load/legacy-xls-sheet.d.ts.map +1 -1
- package/dist/xlsx/load/legacy-xls.d.ts.map +1 -1
- package/dist/xlsx/model.d.ts +1 -1
- package/dist/xlsx/model.d.ts.map +1 -1
- package/dist/xlsx/numfmt/decimal.d.ts.map +1 -1
- package/dist/xlsx/read/package.d.ts +22 -22
- package/dist/xlsx/read/package.d.ts.map +1 -1
- package/dist/xlsx/read/password-hash.d.ts.map +1 -1
- package/dist/xlsx/read/sheet-props.d.ts +7 -7
- package/dist/xlsx/read/sheet-props.d.ts.map +1 -1
- package/dist/xlsx/read/theme.d.ts +2 -2
- package/dist/xlsx/read/theme.d.ts.map +1 -1
- package/dist/xlsx/read/worksheet.d.ts +1 -1
- package/dist/xlsx/read/worksheet.d.ts.map +1 -1
- package/dist/xlsx/read/xml-util.d.ts.map +1 -1
- package/dist/xlsx/workbook.d.ts.map +1 -1
- package/dist/xlsx/write/carried-refs.d.ts.map +1 -1
- package/dist/xlsx/write/comments.d.ts.map +1 -1
- package/dist/xlsx/write/doc-props.d.ts.map +1 -1
- package/dist/xlsx/write/drawing.d.ts.map +1 -1
- package/dist/xlsx/write/dynamic-array.d.ts.map +1 -1
- package/dist/xlsx/write/metadata.d.ts.map +1 -1
- package/dist/xlsx/write/package-writer.d.ts.map +1 -1
- package/dist/xlsx/write/shared-strings.d.ts.map +1 -1
- package/dist/xlsx/write/snapshot.d.ts.map +1 -1
- package/dist/xlsx/write/style-xml.d.ts.map +1 -1
- package/dist/xlsx/write/styles.d.ts.map +1 -1
- package/dist/xlsx/write/vml-ids.d.ts.map +1 -1
- package/dist/xlsx/write/xml-out.d.ts.map +1 -1
- package/dist/xml/namespaces.d.ts +31 -31
- package/dist/xml/namespaces.d.ts.map +1 -1
- package/dist/xml/xml.d.ts.map +1 -1
- package/package.json +19 -14
- package/dist/pptx/chunks/index-BWRd8p4y.d.ts.map +0 -1
- package/dist/pptx/chunks/index-CT9y69AZ.d.ts.map +0 -1
|
@@ -0,0 +1,2207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var Y = require('yjs');
|
|
4
|
+
var awarenessProtocol = require('y-protocols/awareness');
|
|
5
|
+
var decoding = require('lib0/decoding');
|
|
6
|
+
var encoding = require('lib0/encoding');
|
|
7
|
+
|
|
8
|
+
function _interopNamespace(e) {
|
|
9
|
+
if (e && e.__esModule) return e;
|
|
10
|
+
var n = Object.create(null);
|
|
11
|
+
if (e) {
|
|
12
|
+
Object.keys(e).forEach(function (k) {
|
|
13
|
+
if (k !== 'default') {
|
|
14
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
15
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
get: function () { return e[k]; }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
n.default = e;
|
|
23
|
+
return Object.freeze(n);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var Y__namespace = /*#__PURE__*/_interopNamespace(Y);
|
|
27
|
+
var awarenessProtocol__namespace = /*#__PURE__*/_interopNamespace(awarenessProtocol);
|
|
28
|
+
var decoding__namespace = /*#__PURE__*/_interopNamespace(decoding);
|
|
29
|
+
var encoding__namespace = /*#__PURE__*/_interopNamespace(encoding);
|
|
30
|
+
|
|
31
|
+
// src/collab/emitter.ts
|
|
32
|
+
var Emitter = class {
|
|
33
|
+
listeners = /* @__PURE__ */ new Map();
|
|
34
|
+
on(event, listener) {
|
|
35
|
+
let set = this.listeners.get(event);
|
|
36
|
+
if (!set) this.listeners.set(event, set = /* @__PURE__ */ new Set());
|
|
37
|
+
set.add(listener);
|
|
38
|
+
return () => this.off(event, listener);
|
|
39
|
+
}
|
|
40
|
+
off(event, listener) {
|
|
41
|
+
this.listeners.get(event)?.delete(listener);
|
|
42
|
+
}
|
|
43
|
+
emit(event, payload) {
|
|
44
|
+
for (const listener of [...this.listeners.get(event) ?? []]) {
|
|
45
|
+
try {
|
|
46
|
+
listener(payload);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
throw error;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
clear() {
|
|
55
|
+
this.listeners.clear();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/teams/peer.ts
|
|
60
|
+
function createPeer(selfId, peerId, factory, createStream, config, tracks, callbacks) {
|
|
61
|
+
const pc = factory(config);
|
|
62
|
+
const polite = selfId < peerId;
|
|
63
|
+
let makingOffer = false;
|
|
64
|
+
let ignoreOffer = false;
|
|
65
|
+
let closed = false;
|
|
66
|
+
const fail = (error) => {
|
|
67
|
+
if (!closed) callbacks.onError(error instanceof Error ? error : new Error(String(error)));
|
|
68
|
+
};
|
|
69
|
+
const audio = pc.addTransceiver("audio", { direction: "sendrecv" });
|
|
70
|
+
const video = pc.addTransceiver("video", { direction: "sendrecv" });
|
|
71
|
+
if (tracks.audio) void audio.sender.replaceTrack(tracks.audio).catch(fail);
|
|
72
|
+
if (tracks.video) void video.sender.replaceTrack(tracks.video).catch(fail);
|
|
73
|
+
let remote = null;
|
|
74
|
+
pc.ontrack = ({ track, streams }) => {
|
|
75
|
+
const stream = streams[0] ?? remote ?? createStream();
|
|
76
|
+
if (!streams[0] && !stream.getTracks().includes(track)) stream.addTrack(track);
|
|
77
|
+
if (stream !== remote) {
|
|
78
|
+
remote = stream;
|
|
79
|
+
callbacks.onStream(stream);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
pc.onicecandidate = ({ candidate }) => {
|
|
83
|
+
if (candidate) callbacks.sendCandidate(candidate);
|
|
84
|
+
};
|
|
85
|
+
pc.onconnectionstatechange = () => {
|
|
86
|
+
callbacks.onConnection(pc.connectionState);
|
|
87
|
+
if (pc.connectionState === "failed") pc.restartIce?.();
|
|
88
|
+
};
|
|
89
|
+
pc.onnegotiationneeded = () => {
|
|
90
|
+
void (async () => {
|
|
91
|
+
try {
|
|
92
|
+
makingOffer = true;
|
|
93
|
+
await pc.setLocalDescription();
|
|
94
|
+
const d = pc.localDescription;
|
|
95
|
+
if (d && (d.type === "offer" || d.type === "answer") && d.sdp)
|
|
96
|
+
callbacks.sendDescription({ type: d.type, sdp: d.sdp });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
fail(error);
|
|
99
|
+
} finally {
|
|
100
|
+
makingOffer = false;
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
async setTracks(next) {
|
|
106
|
+
if ("audio" in next) await audio.sender.replaceTrack(next.audio ?? null);
|
|
107
|
+
if ("video" in next) await video.sender.replaceTrack(next.video ?? null);
|
|
108
|
+
},
|
|
109
|
+
async receiveDescription(description) {
|
|
110
|
+
try {
|
|
111
|
+
const collision = description.type === "offer" && (makingOffer || pc.signalingState !== "stable");
|
|
112
|
+
ignoreOffer = !polite && collision;
|
|
113
|
+
if (ignoreOffer) return;
|
|
114
|
+
await pc.setRemoteDescription(description);
|
|
115
|
+
if (description.type === "offer") {
|
|
116
|
+
await pc.setLocalDescription();
|
|
117
|
+
const d = pc.localDescription;
|
|
118
|
+
if (d?.sdp && (d.type === "answer" || d.type === "offer"))
|
|
119
|
+
callbacks.sendDescription({ type: d.type, sdp: d.sdp });
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
fail(error);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
async receiveCandidate(candidate) {
|
|
126
|
+
try {
|
|
127
|
+
await pc.addIceCandidate(candidate);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (!ignoreOffer) fail(error);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
close() {
|
|
133
|
+
closed = true;
|
|
134
|
+
pc.onnegotiationneeded = pc.onicecandidate = pc.ontrack = pc.onconnectionstatechange = null;
|
|
135
|
+
pc.close();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/teams/call.ts
|
|
141
|
+
var EMPTY = { audio: false, video: false, screen: false, hand: false };
|
|
142
|
+
var asConnection = (s) => s === "connected" || s === "connecting" || s === "disconnected" || s === "failed" || s === "closed" ? s : "new";
|
|
143
|
+
function defaults() {
|
|
144
|
+
const g = globalThis;
|
|
145
|
+
const PC = g.RTCPeerConnection;
|
|
146
|
+
const MS = g.MediaStream;
|
|
147
|
+
const nav = g.navigator;
|
|
148
|
+
return {
|
|
149
|
+
pc: (config) => {
|
|
150
|
+
if (!PC) throw new Error("RTCPeerConnection is not available in this environment");
|
|
151
|
+
return new PC(config);
|
|
152
|
+
},
|
|
153
|
+
stream: () => {
|
|
154
|
+
if (!MS) throw new Error("MediaStream is not available in this environment");
|
|
155
|
+
return new MS();
|
|
156
|
+
},
|
|
157
|
+
...nav?.mediaDevices ? { media: nav.mediaDevices } : {}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function createCallSession(options) {
|
|
161
|
+
const env = defaults();
|
|
162
|
+
const makePc = options.createPeerConnection ?? env.pc;
|
|
163
|
+
const makeStream = options.createStream ?? env.stream;
|
|
164
|
+
const media = options.mediaDevices ?? env.media;
|
|
165
|
+
const maxParticipants = options.maxParticipants ?? 12;
|
|
166
|
+
const events = new Emitter();
|
|
167
|
+
const self = options.user;
|
|
168
|
+
const remotes = /* @__PURE__ */ new Map();
|
|
169
|
+
const local = {
|
|
170
|
+
audio: null,
|
|
171
|
+
camera: null,
|
|
172
|
+
screen: null
|
|
173
|
+
};
|
|
174
|
+
let state = { ...EMPTY };
|
|
175
|
+
let status = "idle";
|
|
176
|
+
let selfStream = null;
|
|
177
|
+
const setStatus = (next) => {
|
|
178
|
+
status = next;
|
|
179
|
+
events.emit("status", next);
|
|
180
|
+
};
|
|
181
|
+
const videoTrack = () => local.screen ?? local.camera;
|
|
182
|
+
const rebuildSelfStream = () => {
|
|
183
|
+
const tracks = [local.audio, videoTrack()].filter((t) => t !== null);
|
|
184
|
+
if (tracks.length === 0) return void (selfStream = null);
|
|
185
|
+
const stream = makeStream();
|
|
186
|
+
for (const t of tracks) stream.addTrack(t);
|
|
187
|
+
selfStream = stream;
|
|
188
|
+
};
|
|
189
|
+
const list = () => [
|
|
190
|
+
{
|
|
191
|
+
id: self.id,
|
|
192
|
+
name: self.name,
|
|
193
|
+
self: true,
|
|
194
|
+
state,
|
|
195
|
+
connection: "connected",
|
|
196
|
+
stream: selfStream
|
|
197
|
+
},
|
|
198
|
+
...[...remotes].map(([id, r]) => ({
|
|
199
|
+
id,
|
|
200
|
+
name: r.name,
|
|
201
|
+
self: false,
|
|
202
|
+
state: r.state,
|
|
203
|
+
connection: r.connection,
|
|
204
|
+
stream: r.stream
|
|
205
|
+
}))
|
|
206
|
+
];
|
|
207
|
+
const publish = () => events.emit("participants", list());
|
|
208
|
+
const send = (signal) => options.signaling.send(signal);
|
|
209
|
+
const announceState = () => send({ type: "state", from: self.id, state });
|
|
210
|
+
const fail = (error) => events.emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
211
|
+
const ensurePeer = (id, name, remoteState) => {
|
|
212
|
+
const existing = remotes.get(id);
|
|
213
|
+
if (existing) {
|
|
214
|
+
existing.name = name;
|
|
215
|
+
existing.state = remoteState;
|
|
216
|
+
return existing;
|
|
217
|
+
}
|
|
218
|
+
if (remotes.size + 1 >= maxParticipants) {
|
|
219
|
+
fail(new Error(`Call is full (${maxParticipants} participants)`));
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const config = {
|
|
223
|
+
iceServers: options.iceServers ?? [],
|
|
224
|
+
...options.iceTransportPolicy ? { iceTransportPolicy: options.iceTransportPolicy } : {}
|
|
225
|
+
};
|
|
226
|
+
const remote = {
|
|
227
|
+
name,
|
|
228
|
+
state: remoteState,
|
|
229
|
+
connection: "new",
|
|
230
|
+
stream: null,
|
|
231
|
+
peer: createPeer(
|
|
232
|
+
self.id,
|
|
233
|
+
id,
|
|
234
|
+
makePc,
|
|
235
|
+
makeStream,
|
|
236
|
+
config,
|
|
237
|
+
{ audio: local.audio, video: videoTrack() },
|
|
238
|
+
{
|
|
239
|
+
sendDescription: (description) => send({ type: "description", from: self.id, to: id, description }),
|
|
240
|
+
sendCandidate: (candidate) => send({ type: "candidate", from: self.id, to: id, candidate }),
|
|
241
|
+
onStream: (stream) => {
|
|
242
|
+
remote.stream = stream;
|
|
243
|
+
publish();
|
|
244
|
+
},
|
|
245
|
+
onConnection: (s) => {
|
|
246
|
+
remote.connection = asConnection(s);
|
|
247
|
+
publish();
|
|
248
|
+
},
|
|
249
|
+
onError: fail
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
};
|
|
253
|
+
remotes.set(id, remote);
|
|
254
|
+
return remote;
|
|
255
|
+
};
|
|
256
|
+
const drop = (id) => {
|
|
257
|
+
const r = remotes.get(id);
|
|
258
|
+
if (!r) return;
|
|
259
|
+
r.peer.close();
|
|
260
|
+
remotes.delete(id);
|
|
261
|
+
publish();
|
|
262
|
+
};
|
|
263
|
+
const hello = (to, reply = false) => send({
|
|
264
|
+
type: "hello",
|
|
265
|
+
from: self.id,
|
|
266
|
+
name: self.name,
|
|
267
|
+
state,
|
|
268
|
+
...to ? { to } : {},
|
|
269
|
+
...reply ? { reply } : {}
|
|
270
|
+
});
|
|
271
|
+
const onSignal = (signal) => {
|
|
272
|
+
if (signal.from === self.id) return;
|
|
273
|
+
switch (signal.type) {
|
|
274
|
+
case "hello":
|
|
275
|
+
if (signal.to && signal.to !== self.id) return;
|
|
276
|
+
if (ensurePeer(signal.from, signal.name, signal.state) && !signal.reply)
|
|
277
|
+
hello(signal.from, true);
|
|
278
|
+
publish();
|
|
279
|
+
return;
|
|
280
|
+
case "bye":
|
|
281
|
+
return drop(signal.from);
|
|
282
|
+
case "state": {
|
|
283
|
+
const r = remotes.get(signal.from);
|
|
284
|
+
if (r) r.state = signal.state;
|
|
285
|
+
return publish();
|
|
286
|
+
}
|
|
287
|
+
case "description":
|
|
288
|
+
if (signal.to === self.id)
|
|
289
|
+
void remotes.get(signal.from)?.peer.receiveDescription(signal.description);
|
|
290
|
+
return;
|
|
291
|
+
case "candidate":
|
|
292
|
+
if (signal.to === self.id)
|
|
293
|
+
void remotes.get(signal.from)?.peer.receiveCandidate(signal.candidate);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const setVideoEverywhere = async () => {
|
|
298
|
+
const track = videoTrack();
|
|
299
|
+
await Promise.all([...remotes.values()].map((r) => r.peer.setTracks({ video: track })));
|
|
300
|
+
rebuildSelfStream();
|
|
301
|
+
publish();
|
|
302
|
+
};
|
|
303
|
+
const watchEnd = (track, onEnd) => {
|
|
304
|
+
track.onended = onEnd;
|
|
305
|
+
};
|
|
306
|
+
return {
|
|
307
|
+
roomId: options.roomId,
|
|
308
|
+
get status() {
|
|
309
|
+
return status;
|
|
310
|
+
},
|
|
311
|
+
participants: list,
|
|
312
|
+
on: (event, listener) => events.on(event, listener),
|
|
313
|
+
async join(want = {}) {
|
|
314
|
+
if (status === "joining" || status === "connected") return;
|
|
315
|
+
setStatus("joining");
|
|
316
|
+
const audio = want.audio ?? true;
|
|
317
|
+
const video = want.video ?? true;
|
|
318
|
+
if (media && (audio || video)) {
|
|
319
|
+
let stream = null;
|
|
320
|
+
for (const c of [
|
|
321
|
+
{ audio, video },
|
|
322
|
+
{ audio, video: false },
|
|
323
|
+
{ audio: false, video }
|
|
324
|
+
]) {
|
|
325
|
+
if (!c.audio && !c.video) continue;
|
|
326
|
+
try {
|
|
327
|
+
stream = await media.getUserMedia(c);
|
|
328
|
+
break;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (c.audio && c.video) fail(error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (const t of stream?.getTracks() ?? []) {
|
|
334
|
+
if (t.kind === "audio") local.audio = t;
|
|
335
|
+
else if (t.kind === "video") local.camera = t;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
state = { ...EMPTY, audio: Boolean(local.audio), video: Boolean(local.camera) };
|
|
339
|
+
rebuildSelfStream();
|
|
340
|
+
options.signaling.connect({
|
|
341
|
+
open: () => {
|
|
342
|
+
hello();
|
|
343
|
+
setStatus("connected");
|
|
344
|
+
publish();
|
|
345
|
+
},
|
|
346
|
+
close: () => {
|
|
347
|
+
},
|
|
348
|
+
message: onSignal,
|
|
349
|
+
error: fail
|
|
350
|
+
});
|
|
351
|
+
publish();
|
|
352
|
+
},
|
|
353
|
+
leave() {
|
|
354
|
+
if (status === "left" || status === "idle") return;
|
|
355
|
+
send({ type: "bye", from: self.id });
|
|
356
|
+
for (const id of [...remotes.keys()]) drop(id);
|
|
357
|
+
for (const t of [local.audio, local.camera, local.screen]) t?.stop();
|
|
358
|
+
local.audio = local.camera = local.screen = null;
|
|
359
|
+
selfStream = null;
|
|
360
|
+
options.signaling.disconnect();
|
|
361
|
+
setStatus("left");
|
|
362
|
+
publish();
|
|
363
|
+
},
|
|
364
|
+
toggleMic() {
|
|
365
|
+
if (!local.audio) return;
|
|
366
|
+
local.audio.enabled = !local.audio.enabled;
|
|
367
|
+
state = { ...state, audio: local.audio.enabled };
|
|
368
|
+
announceState();
|
|
369
|
+
publish();
|
|
370
|
+
},
|
|
371
|
+
async toggleCamera() {
|
|
372
|
+
if (local.camera) {
|
|
373
|
+
local.camera.stop();
|
|
374
|
+
local.camera = null;
|
|
375
|
+
} else if (media) {
|
|
376
|
+
try {
|
|
377
|
+
const s = await media.getUserMedia({ video: true });
|
|
378
|
+
local.camera = s.getTracks().find((t) => t.kind === "video") ?? null;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return fail(error);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
state = { ...state, video: Boolean(local.camera) };
|
|
384
|
+
await setVideoEverywhere();
|
|
385
|
+
announceState();
|
|
386
|
+
},
|
|
387
|
+
async startScreenShare() {
|
|
388
|
+
if (local.screen || !media?.getDisplayMedia) return;
|
|
389
|
+
try {
|
|
390
|
+
const s = await media.getDisplayMedia({ video: true });
|
|
391
|
+
const track = s.getTracks().find((t) => t.kind === "video") ?? null;
|
|
392
|
+
if (!track) return;
|
|
393
|
+
local.screen = track;
|
|
394
|
+
watchEnd(track, () => void this.stopScreenShare());
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return fail(error);
|
|
397
|
+
}
|
|
398
|
+
state = { ...state, screen: true };
|
|
399
|
+
await setVideoEverywhere();
|
|
400
|
+
announceState();
|
|
401
|
+
},
|
|
402
|
+
async stopScreenShare() {
|
|
403
|
+
if (!local.screen) return;
|
|
404
|
+
local.screen.stop();
|
|
405
|
+
local.screen = null;
|
|
406
|
+
state = { ...state, screen: false };
|
|
407
|
+
await setVideoEverywhere();
|
|
408
|
+
announceState();
|
|
409
|
+
},
|
|
410
|
+
toggleHand() {
|
|
411
|
+
state = { ...state, hand: !state.hand };
|
|
412
|
+
announceState();
|
|
413
|
+
publish();
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/collab/validation.ts
|
|
419
|
+
var DEFAULT_COLOR = "#4c8bf5";
|
|
420
|
+
var ROOM_ID_REGEX = /^[a-zA-Z0-9_-]{1,128}$/u;
|
|
421
|
+
var HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/u;
|
|
422
|
+
function isValidRoomId(roomId) {
|
|
423
|
+
return typeof roomId === "string" && ROOM_ID_REGEX.test(roomId);
|
|
424
|
+
}
|
|
425
|
+
function validateRoomId(roomId) {
|
|
426
|
+
if (!isValidRoomId(roomId)) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`Invalid collaboration room ID: "${String(roomId)}". Must be 1-128 alphanumeric characters, hyphens, or underscores.`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return roomId;
|
|
432
|
+
}
|
|
433
|
+
function isValidId(value, max = 160) {
|
|
434
|
+
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
435
|
+
}
|
|
436
|
+
function stripHtmlTags(text) {
|
|
437
|
+
let result = "";
|
|
438
|
+
let insideTag = false;
|
|
439
|
+
for (const char of text) {
|
|
440
|
+
if (char === "<") insideTag = true;
|
|
441
|
+
else if (char === ">") insideTag = false;
|
|
442
|
+
else if (!insideTag) result += char;
|
|
443
|
+
}
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
function sanitizeUserName(name, fallback = "Anonymous") {
|
|
447
|
+
if (typeof name !== "string") return fallback;
|
|
448
|
+
const trimmed = stripHtmlTags(name).trim().slice(0, 64);
|
|
449
|
+
return trimmed || fallback;
|
|
450
|
+
}
|
|
451
|
+
function sanitizeColor(color, fallback = DEFAULT_COLOR) {
|
|
452
|
+
return typeof color === "string" && HEX_COLOR_REGEX.test(color) ? color : fallback;
|
|
453
|
+
}
|
|
454
|
+
function sanitizeAvatarUrl(url) {
|
|
455
|
+
if (typeof url !== "string") return void 0;
|
|
456
|
+
try {
|
|
457
|
+
const { protocol } = new URL(url);
|
|
458
|
+
if (protocol === "https:" || protocol === "http:" || protocol === "data:") return url;
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
return void 0;
|
|
462
|
+
}
|
|
463
|
+
var LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
|
|
464
|
+
function isMixedContentBlocked(serverUrl, pageProtocol = globalThis.location?.protocol) {
|
|
465
|
+
if (pageProtocol !== "https:") return false;
|
|
466
|
+
let parsed;
|
|
467
|
+
try {
|
|
468
|
+
parsed = new URL(serverUrl);
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
return parsed.protocol === "ws:" && !LOOPBACK_HOSTS.includes(parsed.hostname);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/collab/identity.ts
|
|
476
|
+
var COLLABORATION_ROLES = [
|
|
477
|
+
"owner",
|
|
478
|
+
"collaborator",
|
|
479
|
+
"viewer"
|
|
480
|
+
];
|
|
481
|
+
function sanitizeRole(value) {
|
|
482
|
+
return COLLABORATION_ROLES.includes(value) ? value : void 0;
|
|
483
|
+
}
|
|
484
|
+
function canWrite(role) {
|
|
485
|
+
return role !== "viewer";
|
|
486
|
+
}
|
|
487
|
+
var CURSOR_PALETTE = [
|
|
488
|
+
"#ef4444",
|
|
489
|
+
"#f97316",
|
|
490
|
+
"#eab308",
|
|
491
|
+
"#22c55e",
|
|
492
|
+
"#06b6d4",
|
|
493
|
+
"#3b82f6",
|
|
494
|
+
"#8b5cf6",
|
|
495
|
+
"#ec4899"
|
|
496
|
+
];
|
|
497
|
+
function assignUserColor(seed, palette = CURSOR_PALETTE) {
|
|
498
|
+
if (palette.length === 0) return DEFAULT_COLOR;
|
|
499
|
+
let hash = 0;
|
|
500
|
+
const text = String(seed);
|
|
501
|
+
for (let i = 0; i < text.length; i++) hash = hash * 31 + text.charCodeAt(i) | 0;
|
|
502
|
+
return palette[Math.abs(hash) % palette.length] ?? DEFAULT_COLOR;
|
|
503
|
+
}
|
|
504
|
+
function createClientId() {
|
|
505
|
+
const cryptoApi = globalThis.crypto;
|
|
506
|
+
if (cryptoApi?.randomUUID) return cryptoApi.randomUUID();
|
|
507
|
+
return `c${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
508
|
+
}
|
|
509
|
+
function clientKey(clientId) {
|
|
510
|
+
if (!isValidId(clientId)) throw new Error("Invalid collaboration client ID");
|
|
511
|
+
return Array.from(clientId, (character) => character.codePointAt(0).toString(16)).join("_");
|
|
512
|
+
}
|
|
513
|
+
function createIdGenerator(clientId, prefix = "cid") {
|
|
514
|
+
const key = clientKey(clientId);
|
|
515
|
+
let counter = 0;
|
|
516
|
+
return (kind) => {
|
|
517
|
+
const safeKind = kind.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 24) || "item";
|
|
518
|
+
return `${prefix}-${safeKind}-${key}-${++counter}`;
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/teams/model.ts
|
|
523
|
+
var MAX_MESSAGE_CHARS = 2e4;
|
|
524
|
+
var MAX_CHANNEL_NAME_CHARS = 80;
|
|
525
|
+
var MAX_ATTACHMENTS = 20;
|
|
526
|
+
var MAX_EMOJI_CHARS = 16;
|
|
527
|
+
var OFFICE_EXTENSIONS = {
|
|
528
|
+
docx: "docx",
|
|
529
|
+
docm: "docx",
|
|
530
|
+
dotx: "docx",
|
|
531
|
+
xlsx: "xlsx",
|
|
532
|
+
xlsm: "xlsx",
|
|
533
|
+
xltx: "xlsx",
|
|
534
|
+
pptx: "pptx",
|
|
535
|
+
pptm: "pptx",
|
|
536
|
+
potx: "pptx",
|
|
537
|
+
ppsx: "pptx",
|
|
538
|
+
vsdx: "vsdx",
|
|
539
|
+
vsdm: "vsdx",
|
|
540
|
+
vssx: "vsdx",
|
|
541
|
+
vstx: "vsdx"
|
|
542
|
+
};
|
|
543
|
+
function detectOfficeKind(name, mime) {
|
|
544
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
545
|
+
const byExt = OFFICE_EXTENSIONS[ext];
|
|
546
|
+
if (byExt) return byExt;
|
|
547
|
+
const m = mime?.toLowerCase() ?? "";
|
|
548
|
+
if (m.includes("wordprocessingml")) return "docx";
|
|
549
|
+
if (m.includes("spreadsheetml")) return "xlsx";
|
|
550
|
+
if (m.includes("presentationml")) return "pptx";
|
|
551
|
+
if (m.includes("visio")) return "vsdx";
|
|
552
|
+
return "other";
|
|
553
|
+
}
|
|
554
|
+
function sanitizeMessageText(text) {
|
|
555
|
+
if (typeof text !== "string") return "";
|
|
556
|
+
const cleaned = stripHtmlTags(text).replace(
|
|
557
|
+
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu,
|
|
558
|
+
""
|
|
559
|
+
);
|
|
560
|
+
return cleaned.trim().slice(0, MAX_MESSAGE_CHARS);
|
|
561
|
+
}
|
|
562
|
+
function sanitizeChannelName(name) {
|
|
563
|
+
if (typeof name !== "string") return "";
|
|
564
|
+
return stripHtmlTags(name).replace(/\s+/gu, " ").trim().slice(0, MAX_CHANNEL_NAME_CHARS);
|
|
565
|
+
}
|
|
566
|
+
function sanitizeEmoji(emoji) {
|
|
567
|
+
if (typeof emoji !== "string") return "";
|
|
568
|
+
const cleaned = emoji.trim();
|
|
569
|
+
if (!cleaned || [...cleaned].length > MAX_EMOJI_CHARS || /[\s<>&"'|]/u.test(cleaned)) return "";
|
|
570
|
+
return cleaned;
|
|
571
|
+
}
|
|
572
|
+
function sanitizeAttachment(raw) {
|
|
573
|
+
if (!raw || typeof raw !== "object") return null;
|
|
574
|
+
const r = raw;
|
|
575
|
+
const name = sanitizeChannelName(r.name);
|
|
576
|
+
if (!name) return null;
|
|
577
|
+
const mime = typeof r.mime === "string" && r.mime.length <= 120 ? r.mime : void 0;
|
|
578
|
+
const out = { name, kind: detectOfficeKind(name, mime) };
|
|
579
|
+
if (typeof r.size === "number" && Number.isFinite(r.size) && r.size >= 0) out.size = r.size;
|
|
580
|
+
if (mime) out.mime = mime;
|
|
581
|
+
if (typeof r.url === "string" && /^(https?:\/\/|\/(?!\/))/iu.test(r.url) && r.url.length <= 2048)
|
|
582
|
+
out.url = r.url;
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
function sanitizeAttachments(raw) {
|
|
586
|
+
if (!Array.isArray(raw)) return [];
|
|
587
|
+
const out = [];
|
|
588
|
+
for (const item of raw.slice(0, MAX_ATTACHMENTS)) {
|
|
589
|
+
const a = sanitizeAttachment(item);
|
|
590
|
+
if (a) out.push(a);
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/teams/chat.ts
|
|
596
|
+
var CHANNELS = "teams:channels";
|
|
597
|
+
var REACTIONS = "teams:reactions";
|
|
598
|
+
var messagesMap = (channelId) => `teams:messages:${channelId}`;
|
|
599
|
+
var SEP = "|";
|
|
600
|
+
var isChannelId = (id) => isValidId(id, 64) && /^[\w-]+$/u.test(id);
|
|
601
|
+
function readChannel(id, raw) {
|
|
602
|
+
if (!(raw instanceof Y__namespace.Map)) return null;
|
|
603
|
+
const name = sanitizeChannelName(raw.get("name"));
|
|
604
|
+
if (!name || !isChannelId(id)) return null;
|
|
605
|
+
return {
|
|
606
|
+
id,
|
|
607
|
+
name,
|
|
608
|
+
topic: sanitizeChannelName(raw.get("topic")),
|
|
609
|
+
kind: raw.get("kind") === "direct" ? "direct" : "channel",
|
|
610
|
+
createdAt: Number(raw.get("createdAt")) || 0,
|
|
611
|
+
createdBy: String(raw.get("createdBy") ?? "").slice(0, 160),
|
|
612
|
+
archived: raw.get("archived") === true
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function readMessage(channelId, id, raw, reactions) {
|
|
616
|
+
if (!(raw instanceof Y__namespace.Map) || !isValidId(id)) return null;
|
|
617
|
+
const authorId = raw.get("authorId");
|
|
618
|
+
if (!isValidId(authorId)) return null;
|
|
619
|
+
const deleted = raw.get("deleted") === true;
|
|
620
|
+
const replyTo = raw.get("replyTo");
|
|
621
|
+
const editedAt = Number(raw.get("editedAt"));
|
|
622
|
+
const message = {
|
|
623
|
+
id,
|
|
624
|
+
channelId,
|
|
625
|
+
authorId,
|
|
626
|
+
authorName: sanitizeChannelName(raw.get("authorName")) || "Unknown",
|
|
627
|
+
text: deleted ? "" : sanitizeMessageText(raw.get("text")),
|
|
628
|
+
ts: Number(raw.get("ts")) || 0,
|
|
629
|
+
deleted,
|
|
630
|
+
attachments: deleted ? [] : sanitizeAttachments(raw.get("attachments")),
|
|
631
|
+
reactions
|
|
632
|
+
};
|
|
633
|
+
if (isValidId(replyTo)) message.replyTo = replyTo;
|
|
634
|
+
if (editedAt > 0) message.editedAt = editedAt;
|
|
635
|
+
return message;
|
|
636
|
+
}
|
|
637
|
+
var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "") || "channel";
|
|
638
|
+
function createChatStore(doc, user) {
|
|
639
|
+
const nextId = createIdGenerator(String(doc.clientID), "m");
|
|
640
|
+
const channelMap = doc.getMap(CHANNELS);
|
|
641
|
+
const reactionMap = doc.getMap(REACTIONS);
|
|
642
|
+
const channelMessages = (channelId) => doc.getMap(messagesMap(channelId));
|
|
643
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
644
|
+
let scheduled = false;
|
|
645
|
+
const onUpdate = () => {
|
|
646
|
+
if (scheduled) return;
|
|
647
|
+
scheduled = true;
|
|
648
|
+
queueMicrotask(() => {
|
|
649
|
+
scheduled = false;
|
|
650
|
+
for (const l of [...listeners]) l();
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
doc.on("update", onUpdate);
|
|
654
|
+
const reactionsFor = (messageId) => {
|
|
655
|
+
const out = {};
|
|
656
|
+
const prefix = messageId + SEP;
|
|
657
|
+
reactionMap.forEach((on, key) => {
|
|
658
|
+
if (!on || !key.startsWith(prefix)) return;
|
|
659
|
+
const [, emoji, userId] = key.split(SEP);
|
|
660
|
+
if (!emoji || !userId || !sanitizeEmoji(emoji)) return;
|
|
661
|
+
(out[emoji] ??= []).push(userId);
|
|
662
|
+
});
|
|
663
|
+
return out;
|
|
664
|
+
};
|
|
665
|
+
return {
|
|
666
|
+
channels() {
|
|
667
|
+
const out = [];
|
|
668
|
+
channelMap.forEach((raw, id) => {
|
|
669
|
+
const c = readChannel(id, raw);
|
|
670
|
+
if (c) out.push(c);
|
|
671
|
+
});
|
|
672
|
+
return out.sort((a, b) => a.createdAt - b.createdAt || a.name.localeCompare(b.name));
|
|
673
|
+
},
|
|
674
|
+
createChannel({ name, topic = "", kind = "channel", id }) {
|
|
675
|
+
const clean = sanitizeChannelName(name);
|
|
676
|
+
if (!clean) return null;
|
|
677
|
+
const channelId = id && isChannelId(id) ? id : `${slug(clean)}-${nextId("c").slice(-6)}`;
|
|
678
|
+
if (channelMap.has(channelId)) return readChannel(channelId, channelMap.get(channelId));
|
|
679
|
+
doc.transact(() => {
|
|
680
|
+
const m = new Y__namespace.Map();
|
|
681
|
+
m.set("name", clean);
|
|
682
|
+
m.set("topic", sanitizeChannelName(topic));
|
|
683
|
+
m.set("kind", kind);
|
|
684
|
+
m.set("createdAt", Date.now());
|
|
685
|
+
m.set("createdBy", user.id);
|
|
686
|
+
m.set("archived", false);
|
|
687
|
+
channelMap.set(channelId, m);
|
|
688
|
+
});
|
|
689
|
+
return readChannel(channelId, channelMap.get(channelId));
|
|
690
|
+
},
|
|
691
|
+
archiveChannel(channelId, archived = true) {
|
|
692
|
+
channelMap.get(channelId)?.set("archived", archived);
|
|
693
|
+
},
|
|
694
|
+
messages(channelId) {
|
|
695
|
+
if (!isChannelId(channelId)) return [];
|
|
696
|
+
const out = [];
|
|
697
|
+
channelMessages(channelId).forEach((raw, id) => {
|
|
698
|
+
const m = readMessage(channelId, id, raw, reactionsFor(id));
|
|
699
|
+
if (m) out.push(m);
|
|
700
|
+
});
|
|
701
|
+
return out.sort((a, b) => a.ts - b.ts || a.id.localeCompare(b.id));
|
|
702
|
+
},
|
|
703
|
+
post(channelId, { text, replyTo, attachments }) {
|
|
704
|
+
if (!isChannelId(channelId) || !channelMap.has(channelId)) return null;
|
|
705
|
+
const clean = sanitizeMessageText(text);
|
|
706
|
+
const files = sanitizeAttachments(attachments);
|
|
707
|
+
if (!clean && files.length === 0) return null;
|
|
708
|
+
const id = nextId("m");
|
|
709
|
+
doc.transact(() => {
|
|
710
|
+
const m = new Y__namespace.Map();
|
|
711
|
+
m.set("authorId", user.id);
|
|
712
|
+
m.set("authorName", user.name);
|
|
713
|
+
m.set("text", clean);
|
|
714
|
+
m.set("ts", Date.now());
|
|
715
|
+
m.set("deleted", false);
|
|
716
|
+
if (replyTo && isValidId(replyTo)) m.set("replyTo", replyTo);
|
|
717
|
+
if (files.length) m.set("attachments", files);
|
|
718
|
+
channelMessages(channelId).set(id, m);
|
|
719
|
+
});
|
|
720
|
+
return readMessage(channelId, id, channelMessages(channelId).get(id), {});
|
|
721
|
+
},
|
|
722
|
+
edit(channelId, messageId, text) {
|
|
723
|
+
if (!isChannelId(channelId)) return false;
|
|
724
|
+
const m = channelMessages(channelId).get(messageId);
|
|
725
|
+
const clean = sanitizeMessageText(text);
|
|
726
|
+
if (!m || m.get("authorId") !== user.id || m.get("deleted") === true || !clean) return false;
|
|
727
|
+
doc.transact(() => {
|
|
728
|
+
m.set("text", clean);
|
|
729
|
+
m.set("editedAt", Date.now());
|
|
730
|
+
});
|
|
731
|
+
return true;
|
|
732
|
+
},
|
|
733
|
+
remove(channelId, messageId) {
|
|
734
|
+
if (!isChannelId(channelId)) return false;
|
|
735
|
+
const m = channelMessages(channelId).get(messageId);
|
|
736
|
+
if (!m || m.get("authorId") !== user.id) return false;
|
|
737
|
+
m.set("deleted", true);
|
|
738
|
+
return true;
|
|
739
|
+
},
|
|
740
|
+
react(messageId, emoji) {
|
|
741
|
+
const e = sanitizeEmoji(emoji);
|
|
742
|
+
if (e && isValidId(messageId)) reactionMap.set([messageId, e, user.id].join(SEP), true);
|
|
743
|
+
},
|
|
744
|
+
unreact(messageId, emoji) {
|
|
745
|
+
const e = sanitizeEmoji(emoji);
|
|
746
|
+
if (e) reactionMap.delete([messageId, e, user.id].join(SEP));
|
|
747
|
+
},
|
|
748
|
+
observe(listener) {
|
|
749
|
+
listeners.add(listener);
|
|
750
|
+
return () => listeners.delete(listener);
|
|
751
|
+
},
|
|
752
|
+
destroy() {
|
|
753
|
+
doc.off("update", onUpdate);
|
|
754
|
+
listeners.clear();
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/teams/server-config.ts
|
|
760
|
+
var DEFAULT_ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
761
|
+
var WS_URL = /^wss?:\/\/[^\s/?#]+(?::\d+)?(?:\/[^\s?#]*)?$/iu;
|
|
762
|
+
var ICE_URL = /^(stuns?|turns?):[^\s]+$/iu;
|
|
763
|
+
function wsUrl(value, field, issues) {
|
|
764
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
765
|
+
if (typeof value !== "string" || !WS_URL.test(value.trim())) {
|
|
766
|
+
issues.push(`${field} must be a ws:// or wss:// URL without query or fragment`);
|
|
767
|
+
return void 0;
|
|
768
|
+
}
|
|
769
|
+
return value.trim().replace(/\/+$/u, "");
|
|
770
|
+
}
|
|
771
|
+
function iceServer(raw, index, issues) {
|
|
772
|
+
if (!raw || typeof raw !== "object") {
|
|
773
|
+
issues.push(`iceServers[${index}] must be an object`);
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
const r = raw;
|
|
777
|
+
const urls = (Array.isArray(r.urls) ? r.urls : [r.urls]).filter(
|
|
778
|
+
(u) => typeof u === "string" && ICE_URL.test(u.trim())
|
|
779
|
+
);
|
|
780
|
+
if (urls.length === 0) {
|
|
781
|
+
issues.push(`iceServers[${index}].urls needs a stun: or turn: URL`);
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
const turn = urls.some((u) => /^turns?:/iu.test(u));
|
|
785
|
+
if (turn && (typeof r.username !== "string" || typeof r.credential !== "string")) {
|
|
786
|
+
issues.push(`iceServers[${index}] is a TURN server and needs username and credential`);
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
urls: urls.length === 1 ? urls[0] : urls,
|
|
791
|
+
...typeof r.username === "string" ? { username: r.username } : {},
|
|
792
|
+
...typeof r.credential === "string" ? { credential: r.credential } : {}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function parseServerConfig(input) {
|
|
796
|
+
const issues = [];
|
|
797
|
+
const raw = input && typeof input === "object" ? input : {};
|
|
798
|
+
const syncUrl = wsUrl(raw.syncUrl, "syncUrl", issues);
|
|
799
|
+
const signalingUrl = wsUrl(raw.signalingUrl, "signalingUrl", issues);
|
|
800
|
+
const iceRaw = Array.isArray(raw.iceServers) ? raw.iceServers : void 0;
|
|
801
|
+
const ice = iceRaw ? iceRaw.map((s, i) => iceServer(s, i, issues)).filter((s) => s !== null) : [];
|
|
802
|
+
const mode = raw.mode === "server" && (syncUrl || signalingUrl) ? "server" : "local";
|
|
803
|
+
if (raw.mode === "server" && mode === "local")
|
|
804
|
+
issues.push('mode "server" needs a syncUrl or a signalingUrl; falling back to local');
|
|
805
|
+
const config = {
|
|
806
|
+
mode,
|
|
807
|
+
iceServers: iceRaw ? ice : [...DEFAULT_ICE_SERVERS]
|
|
808
|
+
};
|
|
809
|
+
if (syncUrl) config.syncUrl = syncUrl;
|
|
810
|
+
if (signalingUrl) config.signalingUrl = signalingUrl;
|
|
811
|
+
if (raw.iceTransportPolicy === "relay" || raw.iceTransportPolicy === "all")
|
|
812
|
+
config.iceTransportPolicy = raw.iceTransportPolicy;
|
|
813
|
+
if (typeof raw.token === "string" && raw.token.length > 0 && raw.token.length <= 4096)
|
|
814
|
+
config.token = raw.token;
|
|
815
|
+
if (config.iceTransportPolicy === "relay" && !ice.some((s) => [s.urls].flat().some((u) => /^turns?:/iu.test(u))))
|
|
816
|
+
issues.push('iceTransportPolicy "relay" needs a TURN server, otherwise no call can connect');
|
|
817
|
+
return { config, issues };
|
|
818
|
+
}
|
|
819
|
+
function endpointUrl(base, roomId, token) {
|
|
820
|
+
const url = `${base.replace(/\/+$/u, "")}/${encodeURIComponent(roomId)}`;
|
|
821
|
+
return token ? `${url}?token=${encodeURIComponent(token)}` : url;
|
|
822
|
+
}
|
|
823
|
+
function localServerConfig() {
|
|
824
|
+
return { mode: "local", iceServers: [...DEFAULT_ICE_SERVERS] };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/teams/signaling.ts
|
|
828
|
+
var MAX_SDP_CHARS = 2e5;
|
|
829
|
+
var MAX_SIGNAL_CHARS = 262144;
|
|
830
|
+
var bool = (v) => v === true;
|
|
831
|
+
var parseState = (raw) => {
|
|
832
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
833
|
+
return { audio: bool(r.audio), video: bool(r.video), screen: bool(r.screen), hand: bool(r.hand) };
|
|
834
|
+
};
|
|
835
|
+
function parseSignal(input) {
|
|
836
|
+
let raw = input;
|
|
837
|
+
if (typeof input === "string") {
|
|
838
|
+
if (input.length > MAX_SIGNAL_CHARS) return null;
|
|
839
|
+
try {
|
|
840
|
+
raw = JSON.parse(input);
|
|
841
|
+
} catch {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (!raw || typeof raw !== "object") return null;
|
|
846
|
+
const r = raw;
|
|
847
|
+
if (!isValidId(r.from, 128)) return null;
|
|
848
|
+
const from = r.from;
|
|
849
|
+
const to = isValidId(r.to, 128) ? r.to : void 0;
|
|
850
|
+
switch (r.type) {
|
|
851
|
+
case "hello": {
|
|
852
|
+
const name = typeof r.name === "string" ? r.name.slice(0, 80) : "";
|
|
853
|
+
if (!name) return null;
|
|
854
|
+
const out = { type: "hello", from, name, state: parseState(r.state) };
|
|
855
|
+
if (to) out.to = to;
|
|
856
|
+
if (r.reply === true) out.reply = true;
|
|
857
|
+
return out;
|
|
858
|
+
}
|
|
859
|
+
case "bye":
|
|
860
|
+
return { type: "bye", from };
|
|
861
|
+
case "state":
|
|
862
|
+
return { type: "state", from, state: parseState(r.state) };
|
|
863
|
+
case "description": {
|
|
864
|
+
const d = r.description;
|
|
865
|
+
if (!to || !d || d.type !== "offer" && d.type !== "answer") return null;
|
|
866
|
+
if (typeof d.sdp !== "string" || d.sdp.length > MAX_SDP_CHARS) return null;
|
|
867
|
+
return { type: "description", from, to, description: { type: d.type, sdp: d.sdp } };
|
|
868
|
+
}
|
|
869
|
+
case "candidate": {
|
|
870
|
+
const c = r.candidate;
|
|
871
|
+
if (!to || !c || typeof c.candidate !== "string" || c.candidate.length > 2048) return null;
|
|
872
|
+
const candidate = { candidate: c.candidate };
|
|
873
|
+
if (typeof c.sdpMid === "string" || c.sdpMid === null) candidate.sdpMid = c.sdpMid;
|
|
874
|
+
if (typeof c.sdpMLineIndex === "number" || c.sdpMLineIndex === null)
|
|
875
|
+
candidate.sdpMLineIndex = c.sdpMLineIndex;
|
|
876
|
+
if (typeof c.usernameFragment === "string") candidate.usernameFragment = c.usernameFragment;
|
|
877
|
+
return { type: "candidate", from, to, candidate };
|
|
878
|
+
}
|
|
879
|
+
default:
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function createMemorySignalingHub() {
|
|
884
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
885
|
+
return {
|
|
886
|
+
createChannel(roomId) {
|
|
887
|
+
let mine = null;
|
|
888
|
+
const room = () => {
|
|
889
|
+
let set = rooms.get(roomId);
|
|
890
|
+
if (!set) rooms.set(roomId, set = /* @__PURE__ */ new Set());
|
|
891
|
+
return set;
|
|
892
|
+
};
|
|
893
|
+
return {
|
|
894
|
+
connect(handlers) {
|
|
895
|
+
mine = handlers;
|
|
896
|
+
room().add(handlers);
|
|
897
|
+
queueMicrotask(() => handlers.open());
|
|
898
|
+
},
|
|
899
|
+
send(signal) {
|
|
900
|
+
const wire = parseSignal(JSON.stringify(signal));
|
|
901
|
+
if (!wire) return;
|
|
902
|
+
for (const other of room()) if (other !== mine) queueMicrotask(() => other.message(wire));
|
|
903
|
+
},
|
|
904
|
+
disconnect() {
|
|
905
|
+
if (mine) room().delete(mine);
|
|
906
|
+
mine = null;
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function createBroadcastSignaling(options) {
|
|
913
|
+
let channel = null;
|
|
914
|
+
return {
|
|
915
|
+
connect(handlers) {
|
|
916
|
+
const Impl = options.BroadcastChannel ?? globalThis.BroadcastChannel;
|
|
917
|
+
if (!Impl) return handlers.error(new Error("BroadcastChannel is not available"));
|
|
918
|
+
channel = new Impl(`${options.prefix ?? "teams-signal"}:${options.roomId}`);
|
|
919
|
+
channel.onmessage = (event) => {
|
|
920
|
+
const signal = parseSignal(event.data);
|
|
921
|
+
if (signal) handlers.message(signal);
|
|
922
|
+
};
|
|
923
|
+
handlers.open();
|
|
924
|
+
},
|
|
925
|
+
send(signal) {
|
|
926
|
+
channel?.postMessage(JSON.stringify(signal));
|
|
927
|
+
},
|
|
928
|
+
disconnect() {
|
|
929
|
+
channel?.close();
|
|
930
|
+
channel = null;
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function createWebSocketSignaling(options) {
|
|
935
|
+
const { baseDelayMs = 250, maxDelayMs = 1e4 } = options;
|
|
936
|
+
let socket = null;
|
|
937
|
+
let timer = null;
|
|
938
|
+
let failures = 0;
|
|
939
|
+
let stopped = true;
|
|
940
|
+
let active = null;
|
|
941
|
+
const open = () => {
|
|
942
|
+
if (stopped || !active) return;
|
|
943
|
+
const handlers = active;
|
|
944
|
+
const Impl = options.WebSocket ?? globalThis.WebSocket;
|
|
945
|
+
if (!Impl) return handlers.error(new Error("No WebSocket implementation available"));
|
|
946
|
+
const ws = new Impl(options.url);
|
|
947
|
+
socket = ws;
|
|
948
|
+
ws.onopen = () => {
|
|
949
|
+
failures = 0;
|
|
950
|
+
handlers.open();
|
|
951
|
+
};
|
|
952
|
+
ws.onmessage = (event) => {
|
|
953
|
+
const signal = parseSignal(event.data);
|
|
954
|
+
if (signal) handlers.message(signal);
|
|
955
|
+
};
|
|
956
|
+
ws.onerror = () => handlers.error(new Error("Signaling socket error"));
|
|
957
|
+
ws.onclose = (event) => {
|
|
958
|
+
socket = null;
|
|
959
|
+
handlers.close(event.reason);
|
|
960
|
+
if (stopped) return;
|
|
961
|
+
timer = setTimeout(open, Math.min(maxDelayMs, baseDelayMs * 2 ** failures++));
|
|
962
|
+
};
|
|
963
|
+
};
|
|
964
|
+
return {
|
|
965
|
+
connect(handlers) {
|
|
966
|
+
active = handlers;
|
|
967
|
+
stopped = false;
|
|
968
|
+
open();
|
|
969
|
+
},
|
|
970
|
+
send(signal) {
|
|
971
|
+
if (socket && socket.readyState === 1) socket.send(JSON.stringify(signal));
|
|
972
|
+
},
|
|
973
|
+
disconnect() {
|
|
974
|
+
stopped = true;
|
|
975
|
+
if (timer) clearTimeout(timer);
|
|
976
|
+
timer = null;
|
|
977
|
+
socket?.close();
|
|
978
|
+
socket = null;
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
var MAX_UPDATE_BYTES = 32 * 1024 * 1024;
|
|
983
|
+
function validateUpdate(update, maxBytes = MAX_UPDATE_BYTES) {
|
|
984
|
+
if (!(update instanceof Uint8Array)) return { ok: false, reason: "update must be a Uint8Array" };
|
|
985
|
+
if (update.byteLength === 0) return { ok: false, reason: "update is empty" };
|
|
986
|
+
if (update.byteLength > maxBytes)
|
|
987
|
+
return { ok: false, reason: `update exceeds the ${maxBytes} byte limit` };
|
|
988
|
+
try {
|
|
989
|
+
Y__namespace.decodeUpdate(update);
|
|
990
|
+
return { ok: true };
|
|
991
|
+
} catch (cause) {
|
|
992
|
+
return {
|
|
993
|
+
ok: false,
|
|
994
|
+
reason: `malformed update: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function applyUpdateSafe(doc, update, origin, options = {}) {
|
|
999
|
+
const checked = options.validate === false ? update instanceof Uint8Array && update.byteLength <= (options.maxBytes ?? MAX_UPDATE_BYTES) ? { ok: true } : { ok: false, reason: "invalid or oversized update" } : validateUpdate(update, options.maxBytes);
|
|
1000
|
+
if (!checked.ok) return checked;
|
|
1001
|
+
try {
|
|
1002
|
+
Y__namespace.applyUpdate(doc, update, origin);
|
|
1003
|
+
return { ok: true };
|
|
1004
|
+
} catch (cause) {
|
|
1005
|
+
return {
|
|
1006
|
+
ok: false,
|
|
1007
|
+
reason: `could not apply update: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
var CHUNK = 32768;
|
|
1012
|
+
function toBase64(bytes) {
|
|
1013
|
+
let binary = "";
|
|
1014
|
+
for (let i = 0; i < bytes.length; i += CHUNK)
|
|
1015
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
1016
|
+
return btoa(binary);
|
|
1017
|
+
}
|
|
1018
|
+
function fromBase64(text) {
|
|
1019
|
+
try {
|
|
1020
|
+
const binary = atob(text);
|
|
1021
|
+
const bytes = new Uint8Array(binary.length);
|
|
1022
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1023
|
+
return bytes;
|
|
1024
|
+
} catch {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/teams/view.ts
|
|
1030
|
+
var callRoomId = (workspaceId, channelId) => `call-${workspaceId}-${channelId}`.slice(0, 128);
|
|
1031
|
+
var asAvailability = (v) => v === "busy" || v === "away" ? v : "available";
|
|
1032
|
+
function channelViews(input) {
|
|
1033
|
+
const { channels, messagesOf, selfId, lastRead, peers, workspaceId, selectedId, inCallRoom } = input;
|
|
1034
|
+
return channels.filter((c) => !c.archived).map((c) => {
|
|
1035
|
+
const read = lastRead[c.id] ?? 0;
|
|
1036
|
+
const unread = c.id === selectedId ? 0 : messagesOf(c.id).filter((m) => !m.deleted && m.authorId !== selfId && m.ts > read).length;
|
|
1037
|
+
const room = callRoomId(workspaceId, c.id);
|
|
1038
|
+
return {
|
|
1039
|
+
id: c.id,
|
|
1040
|
+
name: c.name,
|
|
1041
|
+
topic: c.topic,
|
|
1042
|
+
kind: c.kind,
|
|
1043
|
+
unread,
|
|
1044
|
+
live: inCallRoom === room || peers.some((p) => p.callRoom === room)
|
|
1045
|
+
};
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
function peopleViews(self, peers) {
|
|
1049
|
+
return [
|
|
1050
|
+
{ ...self, self: true },
|
|
1051
|
+
...peers.map((p) => ({
|
|
1052
|
+
id: `peer-${p.clientId}`,
|
|
1053
|
+
name: p.userName,
|
|
1054
|
+
color: p.userColor,
|
|
1055
|
+
self: false,
|
|
1056
|
+
availability: asAvailability(p.availability),
|
|
1057
|
+
inCall: Boolean(p.callRoom)
|
|
1058
|
+
}))
|
|
1059
|
+
];
|
|
1060
|
+
}
|
|
1061
|
+
var typingNames = (peers, channelId) => peers.filter((p) => p.typingIn === channelId).map((p) => p.userName);
|
|
1062
|
+
function filesOf(messages) {
|
|
1063
|
+
const out = [];
|
|
1064
|
+
for (const m of messages) {
|
|
1065
|
+
if (m.deleted) continue;
|
|
1066
|
+
for (const a of m.attachments)
|
|
1067
|
+
out.push({
|
|
1068
|
+
name: a.name,
|
|
1069
|
+
kind: a.kind,
|
|
1070
|
+
...a.size !== void 0 ? { size: a.size } : {},
|
|
1071
|
+
...a.url ? { url: a.url } : {},
|
|
1072
|
+
author: m.authorName,
|
|
1073
|
+
ts: m.ts,
|
|
1074
|
+
messageId: m.id
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return out.sort((a, b) => b.ts - a.ts);
|
|
1078
|
+
}
|
|
1079
|
+
function allFilesOf(channels, messagesOf) {
|
|
1080
|
+
return channels.filter((c) => !c.archived).flatMap(
|
|
1081
|
+
(c) => filesOf(messagesOf(c.id)).map((f) => ({ ...f, channelId: c.id, channelName: c.name }))
|
|
1082
|
+
).sort((a, b) => b.ts - a.ts);
|
|
1083
|
+
}
|
|
1084
|
+
function searchMessages(channels, messagesOf, query, limit = 50) {
|
|
1085
|
+
const q = query.trim().toLowerCase();
|
|
1086
|
+
if (!q) return [];
|
|
1087
|
+
const hits = [];
|
|
1088
|
+
for (const c of channels) {
|
|
1089
|
+
if (c.archived) continue;
|
|
1090
|
+
for (const m of messagesOf(c.id))
|
|
1091
|
+
if (!m.deleted && (m.text.toLowerCase().includes(q) || m.authorName.toLowerCase().includes(q)))
|
|
1092
|
+
hits.push({ channelId: c.id, channelName: c.name, message: m });
|
|
1093
|
+
}
|
|
1094
|
+
return hits.sort((a, b) => b.message.ts - a.message.ts).slice(0, limit);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/collab/broadcast-transport.ts
|
|
1098
|
+
var isBytes = (value) => ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === "[object Uint8Array]";
|
|
1099
|
+
var isEnvelope = (value) => {
|
|
1100
|
+
const envelope = value;
|
|
1101
|
+
return !!envelope && typeof envelope.from === "string" && (envelope.kind === "hello" || envelope.kind === "data" && isBytes(envelope.data));
|
|
1102
|
+
};
|
|
1103
|
+
function createBroadcastTransport(options) {
|
|
1104
|
+
const roomId = validateRoomId(options.roomId);
|
|
1105
|
+
const Channel = options.BroadcastChannel ?? globalThis.BroadcastChannel;
|
|
1106
|
+
const self = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
1107
|
+
let channel = null;
|
|
1108
|
+
let handlers = null;
|
|
1109
|
+
const close = () => {
|
|
1110
|
+
if (!channel) return;
|
|
1111
|
+
channel.onmessage = null;
|
|
1112
|
+
channel.close();
|
|
1113
|
+
channel = null;
|
|
1114
|
+
};
|
|
1115
|
+
return {
|
|
1116
|
+
connect(next) {
|
|
1117
|
+
close();
|
|
1118
|
+
handlers = next;
|
|
1119
|
+
if (!Channel) {
|
|
1120
|
+
next.error(new Error("BroadcastChannel is not available in this environment."));
|
|
1121
|
+
next.close("unavailable");
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const open = new Channel(`${options.prefix ?? "ooxml-collab:"}${roomId}`);
|
|
1125
|
+
channel = open;
|
|
1126
|
+
open.onmessage = ({ data }) => {
|
|
1127
|
+
if (channel !== open || !isEnvelope(data) || data.from === self) return;
|
|
1128
|
+
if (data.kind === "hello") handlers?.peer?.();
|
|
1129
|
+
else handlers?.message(new Uint8Array(data.data));
|
|
1130
|
+
};
|
|
1131
|
+
next.open();
|
|
1132
|
+
open.postMessage({ kind: "hello", from: self });
|
|
1133
|
+
},
|
|
1134
|
+
send(data) {
|
|
1135
|
+
try {
|
|
1136
|
+
channel?.postMessage({ kind: "data", from: self, data });
|
|
1137
|
+
} catch (cause) {
|
|
1138
|
+
handlers?.error(cause instanceof Error ? cause : new Error(String(cause)));
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
disconnect() {
|
|
1142
|
+
const was = handlers;
|
|
1143
|
+
const open = channel !== null;
|
|
1144
|
+
close();
|
|
1145
|
+
handlers = null;
|
|
1146
|
+
if (open) was?.close("disconnected");
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/collab/presence.ts
|
|
1152
|
+
var PRESENCE_FIELD = "presence";
|
|
1153
|
+
var STALE_PRESENCE_MS = 3e4;
|
|
1154
|
+
var PRESENCE_HEARTBEAT_MS = 1e4;
|
|
1155
|
+
var BROADCAST_THROTTLE_MS = 50;
|
|
1156
|
+
function sanitizeIdentity(raw) {
|
|
1157
|
+
return {
|
|
1158
|
+
userName: sanitizeUserName(raw.userName),
|
|
1159
|
+
userColor: sanitizeColor(raw.userColor, DEFAULT_COLOR),
|
|
1160
|
+
userAvatar: sanitizeAvatarUrl(raw.userAvatar),
|
|
1161
|
+
role: sanitizeRole(raw.role)
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function isPresenceFresh(lastUpdated, now = Date.now(), staleMs = STALE_PRESENCE_MS) {
|
|
1165
|
+
const updatedAt = new Date(lastUpdated).getTime();
|
|
1166
|
+
return !Number.isNaN(updatedAt) && now - updatedAt <= staleMs;
|
|
1167
|
+
}
|
|
1168
|
+
function derivePresence(states, localClientId, sanitizePayload, options = {}) {
|
|
1169
|
+
const { now = Date.now(), staleMs = STALE_PRESENCE_MS } = options;
|
|
1170
|
+
const users = [];
|
|
1171
|
+
for (const [clientId, state] of states) {
|
|
1172
|
+
if (clientId === localClientId) continue;
|
|
1173
|
+
const raw = state?.[PRESENCE_FIELD];
|
|
1174
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
1175
|
+
const record = raw;
|
|
1176
|
+
const payload = sanitizePayload(record);
|
|
1177
|
+
if (!payload) continue;
|
|
1178
|
+
const lastUpdated = typeof record.lastUpdated === "string" ? record.lastUpdated : "";
|
|
1179
|
+
if (!isPresenceFresh(lastUpdated, now, staleMs)) continue;
|
|
1180
|
+
users.push({ ...sanitizeIdentity(record), clientId, lastUpdated, ...payload });
|
|
1181
|
+
}
|
|
1182
|
+
return users;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/collab/awareness.ts
|
|
1186
|
+
function createPresencePublisher(awareness, identity, initial, options = {}) {
|
|
1187
|
+
const throttleMs = options.throttleMs ?? BROADCAST_THROTTLE_MS;
|
|
1188
|
+
const clock = options.now ?? Date.now;
|
|
1189
|
+
const local = { ...initial };
|
|
1190
|
+
let lastBroadcast = 0;
|
|
1191
|
+
let pending = null;
|
|
1192
|
+
const publish = () => {
|
|
1193
|
+
lastBroadcast = clock();
|
|
1194
|
+
awareness.setLocalStateField(PRESENCE_FIELD, {
|
|
1195
|
+
...identity,
|
|
1196
|
+
...local,
|
|
1197
|
+
lastUpdated: new Date(clock()).toISOString()
|
|
1198
|
+
});
|
|
1199
|
+
};
|
|
1200
|
+
const cancel = () => {
|
|
1201
|
+
if (pending !== null) clearTimeout(pending);
|
|
1202
|
+
pending = null;
|
|
1203
|
+
};
|
|
1204
|
+
const update = (patch) => {
|
|
1205
|
+
const keys = Object.keys(patch);
|
|
1206
|
+
if (!keys.some((key) => patch[key] !== local[key])) return;
|
|
1207
|
+
Object.assign(local, patch);
|
|
1208
|
+
const elapsed = clock() - lastBroadcast;
|
|
1209
|
+
if (elapsed >= throttleMs) {
|
|
1210
|
+
cancel();
|
|
1211
|
+
publish();
|
|
1212
|
+
} else if (pending === null) {
|
|
1213
|
+
pending = setTimeout(() => {
|
|
1214
|
+
pending = null;
|
|
1215
|
+
publish();
|
|
1216
|
+
}, throttleMs - elapsed);
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
publish();
|
|
1220
|
+
lastBroadcast = 0;
|
|
1221
|
+
return {
|
|
1222
|
+
update,
|
|
1223
|
+
flush: () => {
|
|
1224
|
+
cancel();
|
|
1225
|
+
publish();
|
|
1226
|
+
},
|
|
1227
|
+
dispose: cancel
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function removeAwarenessStatesLocally(awareness, clients) {
|
|
1231
|
+
const states = awareness?.states;
|
|
1232
|
+
if (!awareness || !states) return [];
|
|
1233
|
+
const removed = clients.filter((clientId) => states.delete(clientId));
|
|
1234
|
+
if (removed.length > 0) {
|
|
1235
|
+
const change = { added: [], updated: [], removed };
|
|
1236
|
+
awareness.emit?.("change", [change, "peer-departed"]);
|
|
1237
|
+
awareness.emit?.("update", [change, "peer-departed"]);
|
|
1238
|
+
}
|
|
1239
|
+
return removed;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// src/collab/lifecycle.ts
|
|
1243
|
+
var COLLAB_LEAVE_MESSAGE = "ooxml-core:collab-leave";
|
|
1244
|
+
var DEPARTURE_CHANNEL = "ooxml-core:collab-departures";
|
|
1245
|
+
function isLeaveMessage(data, expected) {
|
|
1246
|
+
if (typeof data === "string") return data === expected;
|
|
1247
|
+
return typeof data === "object" && data !== null && data.type === expected;
|
|
1248
|
+
}
|
|
1249
|
+
function registerTeardown(options) {
|
|
1250
|
+
const target = options.target ?? globalThis.window;
|
|
1251
|
+
if (!target) return () => {
|
|
1252
|
+
};
|
|
1253
|
+
const { leave, rejoin } = options;
|
|
1254
|
+
const leaveMessage = options.leaveMessage ?? COLLAB_LEAVE_MESSAGE;
|
|
1255
|
+
let left = false;
|
|
1256
|
+
const doLeave = () => {
|
|
1257
|
+
if (left) return;
|
|
1258
|
+
left = true;
|
|
1259
|
+
leave();
|
|
1260
|
+
};
|
|
1261
|
+
const onPageHide = (event) => {
|
|
1262
|
+
if (event.persisted === true && !rejoin) return;
|
|
1263
|
+
doLeave();
|
|
1264
|
+
};
|
|
1265
|
+
const onBeforeUnload = () => {
|
|
1266
|
+
const allowed = typeof options.leaveOnBeforeUnload === "function" ? options.leaveOnBeforeUnload() : options.leaveOnBeforeUnload !== false;
|
|
1267
|
+
if (allowed) doLeave();
|
|
1268
|
+
};
|
|
1269
|
+
const onPageShow = (event) => {
|
|
1270
|
+
if (event.persisted !== true || !left || !rejoin) return;
|
|
1271
|
+
left = false;
|
|
1272
|
+
rejoin();
|
|
1273
|
+
};
|
|
1274
|
+
const onMessage = (event) => {
|
|
1275
|
+
if (isLeaveMessage(event.data, leaveMessage)) doLeave();
|
|
1276
|
+
};
|
|
1277
|
+
const listeners = {
|
|
1278
|
+
pagehide: onPageHide,
|
|
1279
|
+
beforeunload: onBeforeUnload,
|
|
1280
|
+
pageshow: onPageShow,
|
|
1281
|
+
message: onMessage
|
|
1282
|
+
};
|
|
1283
|
+
for (const [type, listener] of Object.entries(listeners)) target.addEventListener(type, listener);
|
|
1284
|
+
return () => {
|
|
1285
|
+
for (const [type, listener] of Object.entries(listeners))
|
|
1286
|
+
target.removeEventListener(type, listener);
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function defaultFactory(name) {
|
|
1290
|
+
const scope = globalThis;
|
|
1291
|
+
return scope.BroadcastChannel ? new scope.BroadcastChannel(name) : null;
|
|
1292
|
+
}
|
|
1293
|
+
function createDepartureChannel(roomId, awareness, factory, channelName = DEPARTURE_CHANNEL) {
|
|
1294
|
+
const channel = defaultFactory(channelName);
|
|
1295
|
+
if (!channel) return { announce: () => {
|
|
1296
|
+
}, dispose: () => {
|
|
1297
|
+
} };
|
|
1298
|
+
const selfId = awareness.clientID;
|
|
1299
|
+
channel.onmessage = (event) => {
|
|
1300
|
+
const data = event.data;
|
|
1301
|
+
if (typeof data !== "object" || data === null) return;
|
|
1302
|
+
const notice = data;
|
|
1303
|
+
if (notice.channel !== channelName || notice.roomId !== roomId) return;
|
|
1304
|
+
if (typeof notice.clientId !== "number" || notice.clientId === selfId) return;
|
|
1305
|
+
removeAwarenessStatesLocally(awareness, [notice.clientId]);
|
|
1306
|
+
};
|
|
1307
|
+
let closed = false;
|
|
1308
|
+
return {
|
|
1309
|
+
announce: () => {
|
|
1310
|
+
if (closed || typeof selfId !== "number") return;
|
|
1311
|
+
channel.postMessage({ channel: channelName, roomId, clientId: selfId });
|
|
1312
|
+
},
|
|
1313
|
+
dispose: () => {
|
|
1314
|
+
if (closed) return;
|
|
1315
|
+
closed = true;
|
|
1316
|
+
channel.onmessage = null;
|
|
1317
|
+
channel.close();
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/collab/policy.ts
|
|
1323
|
+
var INITIAL_SYNC_GRACE_MS = 3e3;
|
|
1324
|
+
function createSyncGate(onOpen, graceMs = INITIAL_SYNC_GRACE_MS) {
|
|
1325
|
+
let opened = false;
|
|
1326
|
+
let timer = null;
|
|
1327
|
+
const clear = () => {
|
|
1328
|
+
if (timer !== null) clearTimeout(timer);
|
|
1329
|
+
timer = null;
|
|
1330
|
+
};
|
|
1331
|
+
const open = () => {
|
|
1332
|
+
clear();
|
|
1333
|
+
if (opened) return;
|
|
1334
|
+
opened = true;
|
|
1335
|
+
onOpen();
|
|
1336
|
+
};
|
|
1337
|
+
return {
|
|
1338
|
+
isOpen: () => opened,
|
|
1339
|
+
arm: () => {
|
|
1340
|
+
clear();
|
|
1341
|
+
if (!opened) timer = setTimeout(open, graceMs);
|
|
1342
|
+
},
|
|
1343
|
+
open,
|
|
1344
|
+
reset: () => {
|
|
1345
|
+
clear();
|
|
1346
|
+
opened = false;
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/collab/provider.ts
|
|
1352
|
+
var CONNECTION_TIMEOUT_MS = 3e4;
|
|
1353
|
+
|
|
1354
|
+
// src/collab/session.ts
|
|
1355
|
+
function createCollabSession(options) {
|
|
1356
|
+
const roomId = validateRoomId(options.roomId);
|
|
1357
|
+
const ownsDoc = !options.doc;
|
|
1358
|
+
const doc = options.doc ?? new Y__namespace.Doc();
|
|
1359
|
+
const awareness = new awarenessProtocol.Awareness(doc);
|
|
1360
|
+
const events = new Emitter();
|
|
1361
|
+
const role = options.user.role;
|
|
1362
|
+
const identity = {
|
|
1363
|
+
userName: sanitizeUserName(options.user.name),
|
|
1364
|
+
userColor: sanitizeColor(options.user.color, assignUserColor(doc.clientID)),
|
|
1365
|
+
userAvatar: options.user.avatar,
|
|
1366
|
+
role
|
|
1367
|
+
};
|
|
1368
|
+
const sanitizePayload = options.sanitizePayload ?? (() => ({}));
|
|
1369
|
+
const publisher = createPresencePublisher(
|
|
1370
|
+
awareness,
|
|
1371
|
+
identity,
|
|
1372
|
+
options.initialPresence ?? {}
|
|
1373
|
+
);
|
|
1374
|
+
let destroyed = false;
|
|
1375
|
+
let timeout = null;
|
|
1376
|
+
const gate = createSyncGate(() => {
|
|
1377
|
+
if (destroyed) return;
|
|
1378
|
+
options.onReady?.({ synced: provider.synced });
|
|
1379
|
+
events.emit("ready", void 0);
|
|
1380
|
+
}, options.syncGraceMs ?? INITIAL_SYNC_GRACE_MS);
|
|
1381
|
+
const peers = () => derivePresence(awareness.getStates(), doc.clientID, sanitizePayload);
|
|
1382
|
+
const emitPeers = () => events.emit("peers", peers());
|
|
1383
|
+
awareness.on("change", emitPeers);
|
|
1384
|
+
const provider = options.provider({ doc, awareness, roomId });
|
|
1385
|
+
const clearTimeoutTimer = () => {
|
|
1386
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
1387
|
+
timeout = null;
|
|
1388
|
+
};
|
|
1389
|
+
provider.on("status", (status) => {
|
|
1390
|
+
clearTimeoutTimer();
|
|
1391
|
+
events.emit("status", status);
|
|
1392
|
+
if (status === "connecting")
|
|
1393
|
+
timeout = setTimeout(() => {
|
|
1394
|
+
events.emit("error", new Error("Collaboration connection timed out"));
|
|
1395
|
+
events.emit("status", "error");
|
|
1396
|
+
}, options.connectionTimeoutMs ?? CONNECTION_TIMEOUT_MS);
|
|
1397
|
+
if (status === "connected") gate.arm();
|
|
1398
|
+
});
|
|
1399
|
+
provider.on("synced", (synced) => {
|
|
1400
|
+
events.emit("synced", synced);
|
|
1401
|
+
if (synced) gate.open();
|
|
1402
|
+
});
|
|
1403
|
+
provider.on("error", (error) => events.emit("error", error));
|
|
1404
|
+
const heartbeatMs = options.heartbeatMs ?? PRESENCE_HEARTBEAT_MS;
|
|
1405
|
+
const heartbeat = heartbeatMs > 0 ? setInterval(() => {
|
|
1406
|
+
publisher.flush();
|
|
1407
|
+
emitPeers();
|
|
1408
|
+
}, heartbeatMs) : null;
|
|
1409
|
+
heartbeat?.unref?.();
|
|
1410
|
+
const departure = createDepartureChannel(roomId, awareness);
|
|
1411
|
+
const leave = () => {
|
|
1412
|
+
departure.announce();
|
|
1413
|
+
publisher.dispose();
|
|
1414
|
+
awareness.setLocalState(null);
|
|
1415
|
+
provider.disconnect();
|
|
1416
|
+
};
|
|
1417
|
+
const unregisterTeardown = options.teardown ? registerTeardown({
|
|
1418
|
+
leave,
|
|
1419
|
+
rejoin: () => {
|
|
1420
|
+
publisher.flush();
|
|
1421
|
+
provider.connect();
|
|
1422
|
+
},
|
|
1423
|
+
...typeof options.teardown === "object" ? options.teardown : {}
|
|
1424
|
+
}) : () => {
|
|
1425
|
+
};
|
|
1426
|
+
const session = {
|
|
1427
|
+
roomId,
|
|
1428
|
+
doc,
|
|
1429
|
+
awareness,
|
|
1430
|
+
clientId: doc.clientID,
|
|
1431
|
+
role,
|
|
1432
|
+
identity,
|
|
1433
|
+
gate,
|
|
1434
|
+
provider,
|
|
1435
|
+
get status() {
|
|
1436
|
+
return provider.status;
|
|
1437
|
+
},
|
|
1438
|
+
get synced() {
|
|
1439
|
+
return provider.synced;
|
|
1440
|
+
},
|
|
1441
|
+
canWrite: () => !destroyed && gate.isOpen() && canWrite(role),
|
|
1442
|
+
peers,
|
|
1443
|
+
updatePresence: (patch) => publisher.update(patch),
|
|
1444
|
+
on: (event, listener) => events.on(event, listener),
|
|
1445
|
+
connect: () => {
|
|
1446
|
+
if (!destroyed) provider.connect();
|
|
1447
|
+
},
|
|
1448
|
+
disconnect: () => {
|
|
1449
|
+
clearTimeoutTimer();
|
|
1450
|
+
gate.reset();
|
|
1451
|
+
provider.disconnect();
|
|
1452
|
+
},
|
|
1453
|
+
destroy: () => {
|
|
1454
|
+
if (destroyed) return;
|
|
1455
|
+
departure.announce();
|
|
1456
|
+
unregisterTeardown();
|
|
1457
|
+
clearTimeoutTimer();
|
|
1458
|
+
if (heartbeat !== null) clearInterval(heartbeat);
|
|
1459
|
+
publisher.dispose();
|
|
1460
|
+
gate.reset();
|
|
1461
|
+
awareness.setLocalState(null);
|
|
1462
|
+
provider.destroy();
|
|
1463
|
+
departure.dispose();
|
|
1464
|
+
awareness.off("change", emitPeers);
|
|
1465
|
+
awareness.destroy();
|
|
1466
|
+
destroyed = true;
|
|
1467
|
+
events.clear();
|
|
1468
|
+
if (ownsDoc) doc.destroy();
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
if (options.autoConnect !== false) provider.connect();
|
|
1472
|
+
return session;
|
|
1473
|
+
}
|
|
1474
|
+
var MESSAGE_SYNC = 0;
|
|
1475
|
+
var MESSAGE_AWARENESS = 1;
|
|
1476
|
+
var MESSAGE_QUERY_AWARENESS = 3;
|
|
1477
|
+
var SYNC_STEP1 = 0;
|
|
1478
|
+
var SYNC_STEP2 = 1;
|
|
1479
|
+
var SYNC_UPDATE = 2;
|
|
1480
|
+
function transportProvider(options) {
|
|
1481
|
+
return (context) => createTransportProvider(context, options);
|
|
1482
|
+
}
|
|
1483
|
+
function createTransportProvider({ doc, awareness }, { transport, maxMessageBytes = MAX_UPDATE_BYTES }) {
|
|
1484
|
+
const events = new Emitter();
|
|
1485
|
+
let status = "disconnected";
|
|
1486
|
+
let synced = false;
|
|
1487
|
+
let open = false;
|
|
1488
|
+
let destroyed = false;
|
|
1489
|
+
const origin = {};
|
|
1490
|
+
const setStatus = (next) => {
|
|
1491
|
+
if (status === next) return;
|
|
1492
|
+
status = next;
|
|
1493
|
+
events.emit("status", next);
|
|
1494
|
+
};
|
|
1495
|
+
const setSynced = (next) => {
|
|
1496
|
+
if (synced === next) return;
|
|
1497
|
+
synced = next;
|
|
1498
|
+
events.emit("synced", next);
|
|
1499
|
+
};
|
|
1500
|
+
const fail = (reason) => events.emit("error", new Error(reason));
|
|
1501
|
+
const send = (encoder) => {
|
|
1502
|
+
if (open) transport.send(encoding__namespace.toUint8Array(encoder));
|
|
1503
|
+
};
|
|
1504
|
+
const sendStep1 = () => {
|
|
1505
|
+
const encoder = encoding__namespace.createEncoder();
|
|
1506
|
+
encoding__namespace.writeVarUint(encoder, MESSAGE_SYNC);
|
|
1507
|
+
encoding__namespace.writeVarUint(encoder, SYNC_STEP1);
|
|
1508
|
+
encoding__namespace.writeVarUint8Array(encoder, Y__namespace.encodeStateVector(doc));
|
|
1509
|
+
send(encoder);
|
|
1510
|
+
};
|
|
1511
|
+
const sendAwareness = (clients) => {
|
|
1512
|
+
if (clients.length === 0) return;
|
|
1513
|
+
const encoder = encoding__namespace.createEncoder();
|
|
1514
|
+
encoding__namespace.writeVarUint(encoder, MESSAGE_AWARENESS);
|
|
1515
|
+
encoding__namespace.writeVarUint8Array(
|
|
1516
|
+
encoder,
|
|
1517
|
+
awarenessProtocol__namespace.encodeAwarenessUpdate(awareness, clients)
|
|
1518
|
+
);
|
|
1519
|
+
send(encoder);
|
|
1520
|
+
};
|
|
1521
|
+
const handshake = () => {
|
|
1522
|
+
sendStep1();
|
|
1523
|
+
const local = awareness.getLocalState();
|
|
1524
|
+
if (local !== null) awareness.setLocalState(local);
|
|
1525
|
+
};
|
|
1526
|
+
const onDocUpdate = (update, updateOrigin) => {
|
|
1527
|
+
if (updateOrigin === origin || !open) return;
|
|
1528
|
+
const encoder = encoding__namespace.createEncoder();
|
|
1529
|
+
encoding__namespace.writeVarUint(encoder, MESSAGE_SYNC);
|
|
1530
|
+
encoding__namespace.writeVarUint(encoder, SYNC_UPDATE);
|
|
1531
|
+
encoding__namespace.writeVarUint8Array(encoder, update);
|
|
1532
|
+
send(encoder);
|
|
1533
|
+
};
|
|
1534
|
+
const onAwarenessUpdate = (change, updateOrigin) => {
|
|
1535
|
+
if (updateOrigin === origin) return;
|
|
1536
|
+
sendAwareness([...change.added, ...change.updated, ...change.removed]);
|
|
1537
|
+
};
|
|
1538
|
+
const readSync = (decoder) => {
|
|
1539
|
+
const kind = decoding__namespace.readVarUint(decoder);
|
|
1540
|
+
const payload = decoding__namespace.readVarUint8Array(decoder);
|
|
1541
|
+
if (kind === SYNC_STEP1) {
|
|
1542
|
+
const encoder = encoding__namespace.createEncoder();
|
|
1543
|
+
encoding__namespace.writeVarUint(encoder, MESSAGE_SYNC);
|
|
1544
|
+
encoding__namespace.writeVarUint(encoder, SYNC_STEP2);
|
|
1545
|
+
let diff;
|
|
1546
|
+
try {
|
|
1547
|
+
diff = Y__namespace.encodeStateAsUpdate(doc, payload);
|
|
1548
|
+
} catch {
|
|
1549
|
+
return fail("malformed state vector");
|
|
1550
|
+
}
|
|
1551
|
+
encoding__namespace.writeVarUint8Array(encoder, diff);
|
|
1552
|
+
return send(encoder);
|
|
1553
|
+
}
|
|
1554
|
+
if (kind !== SYNC_STEP2 && kind !== SYNC_UPDATE) return fail(`unknown sync message ${kind}`);
|
|
1555
|
+
const result = applyUpdateSafe(doc, payload, origin, { maxBytes: maxMessageBytes });
|
|
1556
|
+
if (!result.ok) return fail(result.reason);
|
|
1557
|
+
if (kind === SYNC_STEP2) setSynced(true);
|
|
1558
|
+
};
|
|
1559
|
+
const onMessage = (data) => {
|
|
1560
|
+
if (destroyed) return;
|
|
1561
|
+
if (!(data instanceof Uint8Array) || data.byteLength === 0 || data.byteLength > maxMessageBytes)
|
|
1562
|
+
return fail("message is empty, not binary, or too large");
|
|
1563
|
+
try {
|
|
1564
|
+
const decoder = decoding__namespace.createDecoder(data);
|
|
1565
|
+
const type = decoding__namespace.readVarUint(decoder);
|
|
1566
|
+
if (type === MESSAGE_SYNC) readSync(decoder);
|
|
1567
|
+
else if (type === MESSAGE_AWARENESS)
|
|
1568
|
+
awarenessProtocol__namespace.applyAwarenessUpdate(
|
|
1569
|
+
awareness,
|
|
1570
|
+
decoding__namespace.readVarUint8Array(decoder),
|
|
1571
|
+
origin
|
|
1572
|
+
);
|
|
1573
|
+
else if (type === MESSAGE_QUERY_AWARENESS) sendAwareness([...awareness.getStates().keys()]);
|
|
1574
|
+
} catch (cause) {
|
|
1575
|
+
fail(`malformed message: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
const dropRemotePeers = () => {
|
|
1579
|
+
const remote = [...awareness.getStates().keys()].filter((id) => id !== doc.clientID);
|
|
1580
|
+
if (remote.length > 0) awarenessProtocol__namespace.removeAwarenessStates(awareness, remote, origin);
|
|
1581
|
+
};
|
|
1582
|
+
const connect = () => {
|
|
1583
|
+
if (destroyed) return;
|
|
1584
|
+
setStatus("connecting");
|
|
1585
|
+
transport.connect({
|
|
1586
|
+
open: () => {
|
|
1587
|
+
if (destroyed) return;
|
|
1588
|
+
open = true;
|
|
1589
|
+
setStatus("connected");
|
|
1590
|
+
handshake();
|
|
1591
|
+
},
|
|
1592
|
+
close: () => {
|
|
1593
|
+
open = false;
|
|
1594
|
+
setSynced(false);
|
|
1595
|
+
dropRemotePeers();
|
|
1596
|
+
if (!destroyed) setStatus("disconnected");
|
|
1597
|
+
},
|
|
1598
|
+
message: onMessage,
|
|
1599
|
+
error: (error) => {
|
|
1600
|
+
events.emit("error", error);
|
|
1601
|
+
if (!open) setStatus("error");
|
|
1602
|
+
},
|
|
1603
|
+
peer: () => {
|
|
1604
|
+
if (open && !destroyed) handshake();
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
};
|
|
1608
|
+
doc.on("update", onDocUpdate);
|
|
1609
|
+
awareness.on("update", onAwarenessUpdate);
|
|
1610
|
+
return {
|
|
1611
|
+
get status() {
|
|
1612
|
+
return status;
|
|
1613
|
+
},
|
|
1614
|
+
get synced() {
|
|
1615
|
+
return synced;
|
|
1616
|
+
},
|
|
1617
|
+
connect,
|
|
1618
|
+
disconnect: () => {
|
|
1619
|
+
if (destroyed) return;
|
|
1620
|
+
const local = awareness.getLocalState();
|
|
1621
|
+
if (open && local !== null) awareness.setLocalState(null);
|
|
1622
|
+
transport.disconnect();
|
|
1623
|
+
open = false;
|
|
1624
|
+
if (local !== null) awareness.setLocalState(local);
|
|
1625
|
+
setSynced(false);
|
|
1626
|
+
dropRemotePeers();
|
|
1627
|
+
setStatus("disconnected");
|
|
1628
|
+
},
|
|
1629
|
+
destroy: () => {
|
|
1630
|
+
if (destroyed) return;
|
|
1631
|
+
if (open) awareness.setLocalState(null);
|
|
1632
|
+
destroyed = true;
|
|
1633
|
+
open = false;
|
|
1634
|
+
doc.off("update", onDocUpdate);
|
|
1635
|
+
awareness.off("update", onAwarenessUpdate);
|
|
1636
|
+
transport.disconnect();
|
|
1637
|
+
events.clear();
|
|
1638
|
+
},
|
|
1639
|
+
on: (event, listener) => events.on(event, listener)
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// src/collab/websocket-transport.ts
|
|
1644
|
+
var OPEN = 1;
|
|
1645
|
+
function createWebSocketTransport(options) {
|
|
1646
|
+
const { baseDelayMs = 250, maxDelayMs = 1e4, maxRetries = Infinity } = options;
|
|
1647
|
+
let socket = null;
|
|
1648
|
+
let handlers = null;
|
|
1649
|
+
let timer = null;
|
|
1650
|
+
let failures = 0;
|
|
1651
|
+
let stopped = true;
|
|
1652
|
+
const open = () => {
|
|
1653
|
+
if (stopped || !handlers) return;
|
|
1654
|
+
const active = handlers;
|
|
1655
|
+
const Impl = options.WebSocket ?? globalThis.WebSocket;
|
|
1656
|
+
if (!Impl) return active.error(new Error("No WebSocket implementation available"));
|
|
1657
|
+
if (isMixedContentBlocked(options.url, options.pageProtocol)) {
|
|
1658
|
+
stopped = true;
|
|
1659
|
+
return active.error(new Error("Insecure ws:// connection blocked from an https page"));
|
|
1660
|
+
}
|
|
1661
|
+
const ws = new Impl(options.url);
|
|
1662
|
+
socket = ws;
|
|
1663
|
+
ws.binaryType = "arraybuffer";
|
|
1664
|
+
ws.onopen = () => {
|
|
1665
|
+
failures = 0;
|
|
1666
|
+
active.open();
|
|
1667
|
+
};
|
|
1668
|
+
ws.onmessage = (event) => {
|
|
1669
|
+
const data = event.data;
|
|
1670
|
+
if (data instanceof ArrayBuffer) active.message(new Uint8Array(data));
|
|
1671
|
+
else if (data instanceof Uint8Array) active.message(data);
|
|
1672
|
+
else active.error(new Error("Received a non-binary WebSocket message"));
|
|
1673
|
+
};
|
|
1674
|
+
ws.onerror = () => active.error(new Error("WebSocket error"));
|
|
1675
|
+
ws.onclose = (event) => {
|
|
1676
|
+
if (socket !== ws) return;
|
|
1677
|
+
socket = null;
|
|
1678
|
+
active.close(event.reason);
|
|
1679
|
+
if (stopped) return;
|
|
1680
|
+
if (++failures > maxRetries) {
|
|
1681
|
+
stopped = true;
|
|
1682
|
+
return active.error(new Error("WebSocket reconnect attempts exhausted"));
|
|
1683
|
+
}
|
|
1684
|
+
timer = setTimeout(open, Math.min(maxDelayMs, baseDelayMs * 2 ** (failures - 1)));
|
|
1685
|
+
};
|
|
1686
|
+
};
|
|
1687
|
+
return {
|
|
1688
|
+
connect(next) {
|
|
1689
|
+
handlers = next;
|
|
1690
|
+
stopped = false;
|
|
1691
|
+
failures = 0;
|
|
1692
|
+
if (timer !== null) clearTimeout(timer);
|
|
1693
|
+
timer = null;
|
|
1694
|
+
if (socket) socket.close();
|
|
1695
|
+
open();
|
|
1696
|
+
},
|
|
1697
|
+
send(data) {
|
|
1698
|
+
if (socket && socket.readyState === OPEN) socket.send(data);
|
|
1699
|
+
},
|
|
1700
|
+
disconnect() {
|
|
1701
|
+
stopped = true;
|
|
1702
|
+
if (timer !== null) clearTimeout(timer);
|
|
1703
|
+
timer = null;
|
|
1704
|
+
const ws = socket;
|
|
1705
|
+
socket = null;
|
|
1706
|
+
if (ws) {
|
|
1707
|
+
ws.onclose = null;
|
|
1708
|
+
ws.close();
|
|
1709
|
+
handlers?.close("disconnected");
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// src/teams/workspace.ts
|
|
1716
|
+
var AVAILABILITY = ["available", "busy", "away"];
|
|
1717
|
+
var safeId = (v) => typeof v === "string" && /^[\w-]{1,64}$/u.test(v) ? v : void 0;
|
|
1718
|
+
function sanitizePresence(raw) {
|
|
1719
|
+
const out = {};
|
|
1720
|
+
const channelId = safeId(raw.channelId);
|
|
1721
|
+
const typingIn = safeId(raw.typingIn);
|
|
1722
|
+
const callRoom = typeof raw.callRoom === "string" && /^[\w-]{1,200}$/u.test(raw.callRoom) ? raw.callRoom : void 0;
|
|
1723
|
+
if (channelId) out.channelId = channelId;
|
|
1724
|
+
if (typingIn) out.typingIn = typingIn;
|
|
1725
|
+
if (callRoom) out.callRoom = callRoom;
|
|
1726
|
+
out.availability = AVAILABILITY.includes(raw.availability) ? raw.availability : "available";
|
|
1727
|
+
return out;
|
|
1728
|
+
}
|
|
1729
|
+
function createTeamsWorkspace(options) {
|
|
1730
|
+
const config = options.config ?? localServerConfig();
|
|
1731
|
+
const user = { id: options.user.id ?? createClientId(), name: options.user.name };
|
|
1732
|
+
const wsCtor = options.WebSocket;
|
|
1733
|
+
const bcCtor = options.BroadcastChannel;
|
|
1734
|
+
const syncTransport = (roomId) => config.mode === "server" && config.syncUrl ? createWebSocketTransport({
|
|
1735
|
+
url: endpointUrl(config.syncUrl, roomId, config.token),
|
|
1736
|
+
...wsCtor ? { WebSocket: wsCtor } : {}
|
|
1737
|
+
}) : createBroadcastTransport({ roomId, ...bcCtor ? { BroadcastChannel: bcCtor } : {} });
|
|
1738
|
+
const signaling = (roomId) => config.mode === "server" && config.signalingUrl ? createWebSocketSignaling({
|
|
1739
|
+
url: endpointUrl(config.signalingUrl, roomId, config.token),
|
|
1740
|
+
...wsCtor ? { WebSocket: wsCtor } : {}
|
|
1741
|
+
}) : createBroadcastSignaling({ roomId, ...bcCtor ? { BroadcastChannel: bcCtor } : {} });
|
|
1742
|
+
let chat;
|
|
1743
|
+
const session = createCollabSession({
|
|
1744
|
+
roomId: options.workspaceId,
|
|
1745
|
+
...options.doc ? { doc: options.doc } : {},
|
|
1746
|
+
provider: transportProvider({ transport: syncTransport(options.workspaceId) }),
|
|
1747
|
+
user: { name: user.name, ...options.user.color ? { color: options.user.color } : {} },
|
|
1748
|
+
initialPresence: { availability: "available" },
|
|
1749
|
+
sanitizePayload: sanitizePresence,
|
|
1750
|
+
teardown: true,
|
|
1751
|
+
// Any writer may seed the default channel; the fixed id makes concurrent seeds converge.
|
|
1752
|
+
onReady: () => {
|
|
1753
|
+
if (chat.channels().length === 0)
|
|
1754
|
+
chat.createChannel({ id: "general", name: "General", topic: "Company-wide announcements" });
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
chat = createChatStore(session.doc, user);
|
|
1758
|
+
const calls = /* @__PURE__ */ new Map();
|
|
1759
|
+
return {
|
|
1760
|
+
user,
|
|
1761
|
+
session,
|
|
1762
|
+
chat,
|
|
1763
|
+
config,
|
|
1764
|
+
call(channelId) {
|
|
1765
|
+
const existing = calls.get(channelId);
|
|
1766
|
+
if (existing && existing.status !== "left") return existing;
|
|
1767
|
+
const roomId = `call-${options.workspaceId}-${channelId}`.slice(0, 128);
|
|
1768
|
+
const call = createCallSession({
|
|
1769
|
+
roomId,
|
|
1770
|
+
user,
|
|
1771
|
+
signaling: signaling(roomId),
|
|
1772
|
+
iceServers: config.iceServers,
|
|
1773
|
+
...config.iceTransportPolicy ? { iceTransportPolicy: config.iceTransportPolicy } : {},
|
|
1774
|
+
...options.mediaDevices ? { mediaDevices: options.mediaDevices } : {},
|
|
1775
|
+
...options.createPeerConnection ? { createPeerConnection: options.createPeerConnection } : {},
|
|
1776
|
+
...options.createStream ? { createStream: options.createStream } : {}
|
|
1777
|
+
});
|
|
1778
|
+
call.on("status", (s) => {
|
|
1779
|
+
if (s === "connected") session.updatePresence({ callRoom: roomId });
|
|
1780
|
+
if (s === "left") session.updatePresence({ callRoom: "" });
|
|
1781
|
+
});
|
|
1782
|
+
calls.set(channelId, call);
|
|
1783
|
+
return call;
|
|
1784
|
+
},
|
|
1785
|
+
setActiveChannel: (channelId) => session.updatePresence({ channelId: channelId ?? "" }),
|
|
1786
|
+
setTyping: (channelId) => session.updatePresence({ typingIn: channelId ?? "" }),
|
|
1787
|
+
setAvailability: (availability) => session.updatePresence({ availability }),
|
|
1788
|
+
destroy() {
|
|
1789
|
+
for (const c of calls.values()) c.leave();
|
|
1790
|
+
chat.destroy();
|
|
1791
|
+
session.destroy();
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// src/teams/store.ts
|
|
1797
|
+
var STORAGE_DOC = "teams:doc:";
|
|
1798
|
+
var STORAGE_READ = "teams:read:";
|
|
1799
|
+
var MAX_SNAPSHOT_CHARS = 3e6;
|
|
1800
|
+
var TYPING_MS = 4e3;
|
|
1801
|
+
function restoreDoc(storage, id) {
|
|
1802
|
+
const doc = new Y__namespace.Doc();
|
|
1803
|
+
try {
|
|
1804
|
+
const bytes = fromBase64(storage?.getItem(STORAGE_DOC + id) ?? "");
|
|
1805
|
+
if (bytes) Y__namespace.applyUpdate(doc, bytes);
|
|
1806
|
+
} catch {
|
|
1807
|
+
}
|
|
1808
|
+
return doc;
|
|
1809
|
+
}
|
|
1810
|
+
function createTeamsClient(options) {
|
|
1811
|
+
const { storage, workspaceId } = options;
|
|
1812
|
+
const doc = restoreDoc(storage, workspaceId);
|
|
1813
|
+
const ws = createTeamsWorkspace({ ...options, doc });
|
|
1814
|
+
const notices = new Emitter();
|
|
1815
|
+
const notice = (text) => notices.emit("notice", text);
|
|
1816
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1817
|
+
let lastRead = {};
|
|
1818
|
+
try {
|
|
1819
|
+
lastRead = JSON.parse(storage?.getItem(STORAGE_READ + workspaceId) ?? "{}");
|
|
1820
|
+
} catch {
|
|
1821
|
+
lastRead = {};
|
|
1822
|
+
}
|
|
1823
|
+
let selected = "";
|
|
1824
|
+
let query = "";
|
|
1825
|
+
let replyId = "";
|
|
1826
|
+
let editId = "";
|
|
1827
|
+
let availability = "available";
|
|
1828
|
+
let typingTimer;
|
|
1829
|
+
let saveTimer;
|
|
1830
|
+
let destroyed = false;
|
|
1831
|
+
let call = null;
|
|
1832
|
+
const media = () => options.mediaDevices ?? globalThis.navigator?.mediaDevices;
|
|
1833
|
+
const messagesOf = (id) => ws.chat.messages(id);
|
|
1834
|
+
const peers = () => ws.session.peers();
|
|
1835
|
+
const visibleChannels = () => ws.chat.channels().filter((c) => !c.archived);
|
|
1836
|
+
const compute = () => {
|
|
1837
|
+
const channels = visibleChannels();
|
|
1838
|
+
if (!channels.some((c) => c.id === selected)) {
|
|
1839
|
+
selected = channels[0]?.id ?? "";
|
|
1840
|
+
if (selected) ws.setActiveChannel(selected);
|
|
1841
|
+
}
|
|
1842
|
+
if (selected) lastRead[selected] = Date.now();
|
|
1843
|
+
const p = peers();
|
|
1844
|
+
const messages = selected ? messagesOf(selected) : [];
|
|
1845
|
+
const channel = channels.find((c) => c.id === selected) ?? null;
|
|
1846
|
+
const session = call?.session ?? null;
|
|
1847
|
+
const participants = session?.participants() ?? [];
|
|
1848
|
+
const channelName = channels.find((c) => c.id === call?.channelId)?.name ?? "";
|
|
1849
|
+
return {
|
|
1850
|
+
status: ws.session.status,
|
|
1851
|
+
mode: ws.config.mode,
|
|
1852
|
+
user: ws.user,
|
|
1853
|
+
availability,
|
|
1854
|
+
channels: channelViews({
|
|
1855
|
+
channels,
|
|
1856
|
+
messagesOf,
|
|
1857
|
+
selfId: ws.user.id,
|
|
1858
|
+
lastRead,
|
|
1859
|
+
peers: p,
|
|
1860
|
+
workspaceId,
|
|
1861
|
+
selectedId: selected,
|
|
1862
|
+
...call?.session && call.phase === "connected" ? { inCallRoom: callRoomId(workspaceId, call.channelId) } : {}
|
|
1863
|
+
}),
|
|
1864
|
+
selectedChannelId: selected,
|
|
1865
|
+
channel,
|
|
1866
|
+
messages,
|
|
1867
|
+
files: filesOf(messages),
|
|
1868
|
+
allFiles: allFilesOf(channels, messagesOf),
|
|
1869
|
+
people: peopleViews(
|
|
1870
|
+
{
|
|
1871
|
+
id: ws.user.id,
|
|
1872
|
+
name: ws.user.name,
|
|
1873
|
+
color: ws.session.identity.userColor,
|
|
1874
|
+
availability,
|
|
1875
|
+
inCall: call?.phase === "connected"
|
|
1876
|
+
},
|
|
1877
|
+
p
|
|
1878
|
+
),
|
|
1879
|
+
typing: selected ? typingNames(p, selected) : [],
|
|
1880
|
+
replyingTo: messages.find((m) => m.id === replyId) ?? null,
|
|
1881
|
+
editing: messages.find((m) => m.id === editId) ?? null,
|
|
1882
|
+
searchQuery: query,
|
|
1883
|
+
searchResults: searchMessages(channels, messagesOf, query),
|
|
1884
|
+
call: call ? {
|
|
1885
|
+
channelId: call.channelId,
|
|
1886
|
+
channelName,
|
|
1887
|
+
phase: call.phase,
|
|
1888
|
+
prejoin: call.prejoin,
|
|
1889
|
+
preview: call.preview,
|
|
1890
|
+
participants,
|
|
1891
|
+
self: participants.find((x) => x.self),
|
|
1892
|
+
startedAt: call.startedAt
|
|
1893
|
+
} : null
|
|
1894
|
+
};
|
|
1895
|
+
};
|
|
1896
|
+
let state = compute();
|
|
1897
|
+
let queued = false;
|
|
1898
|
+
const refresh = () => {
|
|
1899
|
+
if (queued || destroyed) return;
|
|
1900
|
+
queued = true;
|
|
1901
|
+
queueMicrotask(() => {
|
|
1902
|
+
queued = false;
|
|
1903
|
+
if (destroyed) return;
|
|
1904
|
+
state = compute();
|
|
1905
|
+
for (const l of [...listeners]) l();
|
|
1906
|
+
});
|
|
1907
|
+
};
|
|
1908
|
+
const scheduleSave = () => {
|
|
1909
|
+
if (!storage) return;
|
|
1910
|
+
clearTimeout(saveTimer);
|
|
1911
|
+
saveTimer = setTimeout(() => {
|
|
1912
|
+
try {
|
|
1913
|
+
const text = toBase64(Y__namespace.encodeStateAsUpdate(doc));
|
|
1914
|
+
if (text.length <= MAX_SNAPSHOT_CHARS) storage.setItem(STORAGE_DOC + workspaceId, text);
|
|
1915
|
+
storage.setItem(STORAGE_READ + workspaceId, JSON.stringify(lastRead));
|
|
1916
|
+
} catch {
|
|
1917
|
+
}
|
|
1918
|
+
}, 800);
|
|
1919
|
+
};
|
|
1920
|
+
doc.on("update", scheduleSave);
|
|
1921
|
+
const unsubs = [
|
|
1922
|
+
ws.chat.observe(refresh),
|
|
1923
|
+
ws.session.on("peers", refresh),
|
|
1924
|
+
ws.session.on("status", refresh),
|
|
1925
|
+
ws.session.on("error", (e) => notice(e.message))
|
|
1926
|
+
];
|
|
1927
|
+
const stopPreview = () => {
|
|
1928
|
+
for (const t of call?.preview?.getTracks() ?? []) t.stop();
|
|
1929
|
+
if (call) call.preview = null;
|
|
1930
|
+
};
|
|
1931
|
+
const startPreview = async () => {
|
|
1932
|
+
if (!call || call.phase !== "prejoin" || call.preview || !call.prejoin.video) return;
|
|
1933
|
+
try {
|
|
1934
|
+
const stream = await media()?.getUserMedia({ video: true });
|
|
1935
|
+
if (!stream) return;
|
|
1936
|
+
if (call?.phase === "prejoin" && call.prejoin.video) call.preview = stream;
|
|
1937
|
+
else for (const t of stream.getTracks()) t.stop();
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
notice(error instanceof Error ? error.message : "Camera unavailable");
|
|
1940
|
+
if (call) call.prejoin = { ...call.prejoin, video: false };
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
const server = () => {
|
|
1944
|
+
const { config } = ws;
|
|
1945
|
+
if (config.mode !== "server" || !config.syncUrl) return null;
|
|
1946
|
+
const u = new URL(config.syncUrl);
|
|
1947
|
+
u.protocol = u.protocol === "wss:" ? "https:" : "http:";
|
|
1948
|
+
return { base: u.origin, token: config.token };
|
|
1949
|
+
};
|
|
1950
|
+
const upload = async (file) => {
|
|
1951
|
+
const attachment = {
|
|
1952
|
+
name: file.name,
|
|
1953
|
+
kind: "other",
|
|
1954
|
+
size: file.size,
|
|
1955
|
+
...file.type ? { mime: file.type } : {}
|
|
1956
|
+
};
|
|
1957
|
+
try {
|
|
1958
|
+
if (options.uploadFile) {
|
|
1959
|
+
attachment.url = (await options.uploadFile(file, { workspaceId })).url;
|
|
1960
|
+
return attachment;
|
|
1961
|
+
}
|
|
1962
|
+
const target = server();
|
|
1963
|
+
if (!target) {
|
|
1964
|
+
notice("No file server configured: sharing the name only");
|
|
1965
|
+
return attachment;
|
|
1966
|
+
}
|
|
1967
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
1968
|
+
const res = await doFetch(
|
|
1969
|
+
`${target.base}/files/${workspaceId}/${encodeURIComponent(file.name)}`,
|
|
1970
|
+
{
|
|
1971
|
+
method: "POST",
|
|
1972
|
+
body: file,
|
|
1973
|
+
...target.token ? { headers: { Authorization: `Bearer ${target.token}` } } : {}
|
|
1974
|
+
}
|
|
1975
|
+
);
|
|
1976
|
+
if (!res.ok) throw new Error(`Upload failed (${res.status})`);
|
|
1977
|
+
attachment.url = `${target.base}${(await res.json()).url}`;
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
notice(error instanceof Error ? error.message : "Upload failed");
|
|
1980
|
+
}
|
|
1981
|
+
return attachment;
|
|
1982
|
+
};
|
|
1983
|
+
const act = (fn) => (...args) => {
|
|
1984
|
+
fn(...args);
|
|
1985
|
+
refresh();
|
|
1986
|
+
};
|
|
1987
|
+
const find = (id) => messagesOf(selected).find((m) => m.id === id);
|
|
1988
|
+
const leave = () => {
|
|
1989
|
+
const active = call;
|
|
1990
|
+
if (!active) return;
|
|
1991
|
+
stopPreview();
|
|
1992
|
+
active.session?.leave();
|
|
1993
|
+
for (const f of active.off) f();
|
|
1994
|
+
call = null;
|
|
1995
|
+
};
|
|
1996
|
+
return {
|
|
1997
|
+
getState: () => state,
|
|
1998
|
+
subscribe(listener) {
|
|
1999
|
+
listeners.add(listener);
|
|
2000
|
+
return () => listeners.delete(listener);
|
|
2001
|
+
},
|
|
2002
|
+
on: (event, listener) => notices.on(event, listener),
|
|
2003
|
+
workspace: ws,
|
|
2004
|
+
select: act((id) => {
|
|
2005
|
+
if (id === selected) return;
|
|
2006
|
+
selected = id;
|
|
2007
|
+
replyId = editId = "";
|
|
2008
|
+
ws.setActiveChannel(id);
|
|
2009
|
+
scheduleSave();
|
|
2010
|
+
}),
|
|
2011
|
+
createChannel: act((name, topic = "") => {
|
|
2012
|
+
const channel = ws.chat.createChannel({ name, topic });
|
|
2013
|
+
if (channel) {
|
|
2014
|
+
selected = channel.id;
|
|
2015
|
+
ws.setActiveChannel(channel.id);
|
|
2016
|
+
}
|
|
2017
|
+
}),
|
|
2018
|
+
async send({ text, files = [] }) {
|
|
2019
|
+
if (!selected) return;
|
|
2020
|
+
clearTimeout(typingTimer);
|
|
2021
|
+
ws.setTyping(void 0);
|
|
2022
|
+
if (editId) {
|
|
2023
|
+
if (!ws.chat.edit(selected, editId, text)) notice("Only your own messages can be edited");
|
|
2024
|
+
editId = "";
|
|
2025
|
+
return refresh();
|
|
2026
|
+
}
|
|
2027
|
+
const attachments = [];
|
|
2028
|
+
for (const file of files) attachments.push(await upload(file));
|
|
2029
|
+
ws.chat.post(selected, { text, ...replyId ? { replyTo: replyId } : {}, attachments });
|
|
2030
|
+
replyId = "";
|
|
2031
|
+
refresh();
|
|
2032
|
+
},
|
|
2033
|
+
startReply: act((id) => {
|
|
2034
|
+
if (find(id)) [replyId, editId] = [id, ""];
|
|
2035
|
+
}),
|
|
2036
|
+
startEdit: act((id) => {
|
|
2037
|
+
const m = find(id);
|
|
2038
|
+
if (m && m.authorId === ws.user.id) [editId, replyId] = [id, ""];
|
|
2039
|
+
}),
|
|
2040
|
+
cancelCompose: act(() => {
|
|
2041
|
+
replyId = editId = "";
|
|
2042
|
+
}),
|
|
2043
|
+
deleteMessage: act((id) => void ws.chat.remove(selected, id)),
|
|
2044
|
+
toggleReaction: act((id, emoji) => {
|
|
2045
|
+
const mine = find(id)?.reactions[emoji]?.includes(ws.user.id);
|
|
2046
|
+
if (mine) ws.chat.unreact(id, emoji);
|
|
2047
|
+
else ws.chat.react(id, emoji);
|
|
2048
|
+
}),
|
|
2049
|
+
notifyTyping() {
|
|
2050
|
+
if (!selected) return;
|
|
2051
|
+
ws.setTyping(selected);
|
|
2052
|
+
clearTimeout(typingTimer);
|
|
2053
|
+
typingTimer = setTimeout(() => ws.setTyping(void 0), TYPING_MS);
|
|
2054
|
+
},
|
|
2055
|
+
setAvailability: act((value) => {
|
|
2056
|
+
availability = value;
|
|
2057
|
+
ws.setAvailability(value);
|
|
2058
|
+
}),
|
|
2059
|
+
search: act((q) => {
|
|
2060
|
+
query = q;
|
|
2061
|
+
}),
|
|
2062
|
+
async fileUrl(attachment) {
|
|
2063
|
+
const target = server();
|
|
2064
|
+
const raw = attachment.url;
|
|
2065
|
+
if (!raw || !target?.token) return raw;
|
|
2066
|
+
let url;
|
|
2067
|
+
try {
|
|
2068
|
+
url = new URL(raw);
|
|
2069
|
+
} catch {
|
|
2070
|
+
return raw;
|
|
2071
|
+
}
|
|
2072
|
+
if (url.origin !== target.base || url.username || url.password) return raw;
|
|
2073
|
+
if (!url.pathname.startsWith("/files/")) return raw;
|
|
2074
|
+
try {
|
|
2075
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
2076
|
+
const res = await doFetch(
|
|
2077
|
+
`${target.base}/files/link${url.pathname.slice("/files".length)}`,
|
|
2078
|
+
{
|
|
2079
|
+
method: "POST",
|
|
2080
|
+
headers: { Authorization: `Bearer ${target.token}` }
|
|
2081
|
+
}
|
|
2082
|
+
);
|
|
2083
|
+
if (!res.ok) throw new Error(`Link failed (${res.status})`);
|
|
2084
|
+
const { url: signed } = await res.json();
|
|
2085
|
+
return `${target.base}${signed}`;
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
notice(error instanceof Error ? error.message : "Could not open the file");
|
|
2088
|
+
return void 0;
|
|
2089
|
+
}
|
|
2090
|
+
},
|
|
2091
|
+
async openCall(channelId = selected) {
|
|
2092
|
+
if (!channelId) return;
|
|
2093
|
+
if (call?.channelId === channelId) return;
|
|
2094
|
+
leave();
|
|
2095
|
+
if (channelId !== selected) {
|
|
2096
|
+
selected = channelId;
|
|
2097
|
+
ws.setActiveChannel(channelId);
|
|
2098
|
+
}
|
|
2099
|
+
call = {
|
|
2100
|
+
channelId,
|
|
2101
|
+
phase: "prejoin",
|
|
2102
|
+
prejoin: { audio: true, video: true },
|
|
2103
|
+
preview: null,
|
|
2104
|
+
session: null,
|
|
2105
|
+
startedAt: null,
|
|
2106
|
+
off: []
|
|
2107
|
+
};
|
|
2108
|
+
refresh();
|
|
2109
|
+
await startPreview();
|
|
2110
|
+
refresh();
|
|
2111
|
+
},
|
|
2112
|
+
async setPrejoin(next) {
|
|
2113
|
+
if (!call || call.phase !== "prejoin") return;
|
|
2114
|
+
call.prejoin = { ...call.prejoin, ...next };
|
|
2115
|
+
if (next.video === false) stopPreview();
|
|
2116
|
+
refresh();
|
|
2117
|
+
await startPreview();
|
|
2118
|
+
refresh();
|
|
2119
|
+
},
|
|
2120
|
+
async joinCall() {
|
|
2121
|
+
const active = call;
|
|
2122
|
+
if (!active || active.phase !== "prejoin") return;
|
|
2123
|
+
stopPreview();
|
|
2124
|
+
active.phase = "joining";
|
|
2125
|
+
const session = ws.call(active.channelId);
|
|
2126
|
+
active.session = session;
|
|
2127
|
+
active.off.push(
|
|
2128
|
+
session.on("participants", refresh),
|
|
2129
|
+
session.on("error", (e) => notice(e.message)),
|
|
2130
|
+
session.on("status", (s) => {
|
|
2131
|
+
if (s === "connected") {
|
|
2132
|
+
active.phase = "connected";
|
|
2133
|
+
active.startedAt ??= Date.now();
|
|
2134
|
+
}
|
|
2135
|
+
if (s === "left" && call === active) {
|
|
2136
|
+
for (const f of active.off) f();
|
|
2137
|
+
call = null;
|
|
2138
|
+
}
|
|
2139
|
+
refresh();
|
|
2140
|
+
})
|
|
2141
|
+
);
|
|
2142
|
+
refresh();
|
|
2143
|
+
await session.join(active.prejoin);
|
|
2144
|
+
refresh();
|
|
2145
|
+
},
|
|
2146
|
+
leaveCall: act(leave),
|
|
2147
|
+
toggleMic: act(() => call?.session?.toggleMic()),
|
|
2148
|
+
async toggleCamera() {
|
|
2149
|
+
await call?.session?.toggleCamera();
|
|
2150
|
+
refresh();
|
|
2151
|
+
},
|
|
2152
|
+
async toggleScreenShare() {
|
|
2153
|
+
const s = call?.session;
|
|
2154
|
+
if (!s) return;
|
|
2155
|
+
const sharing = s.participants().find((p) => p.self)?.state.screen;
|
|
2156
|
+
await (sharing ? s.stopScreenShare() : s.startScreenShare());
|
|
2157
|
+
refresh();
|
|
2158
|
+
},
|
|
2159
|
+
toggleHand: act(() => call?.session?.toggleHand()),
|
|
2160
|
+
destroy() {
|
|
2161
|
+
destroyed = true;
|
|
2162
|
+
clearTimeout(typingTimer);
|
|
2163
|
+
clearTimeout(saveTimer);
|
|
2164
|
+
try {
|
|
2165
|
+
storage?.setItem(STORAGE_READ + workspaceId, JSON.stringify(lastRead));
|
|
2166
|
+
} catch {
|
|
2167
|
+
}
|
|
2168
|
+
leave();
|
|
2169
|
+
for (const u of unsubs) u();
|
|
2170
|
+
doc.off("update", scheduleSave);
|
|
2171
|
+
listeners.clear();
|
|
2172
|
+
ws.destroy();
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
exports.DEFAULT_ICE_SERVERS = DEFAULT_ICE_SERVERS;
|
|
2178
|
+
exports.MAX_ATTACHMENTS = MAX_ATTACHMENTS;
|
|
2179
|
+
exports.MAX_CHANNEL_NAME_CHARS = MAX_CHANNEL_NAME_CHARS;
|
|
2180
|
+
exports.MAX_MESSAGE_CHARS = MAX_MESSAGE_CHARS;
|
|
2181
|
+
exports.MAX_SDP_CHARS = MAX_SDP_CHARS;
|
|
2182
|
+
exports.MAX_SIGNAL_CHARS = MAX_SIGNAL_CHARS;
|
|
2183
|
+
exports.allFilesOf = allFilesOf;
|
|
2184
|
+
exports.callRoomId = callRoomId;
|
|
2185
|
+
exports.channelViews = channelViews;
|
|
2186
|
+
exports.createBroadcastSignaling = createBroadcastSignaling;
|
|
2187
|
+
exports.createCallSession = createCallSession;
|
|
2188
|
+
exports.createChatStore = createChatStore;
|
|
2189
|
+
exports.createMemorySignalingHub = createMemorySignalingHub;
|
|
2190
|
+
exports.createPeer = createPeer;
|
|
2191
|
+
exports.createTeamsClient = createTeamsClient;
|
|
2192
|
+
exports.createTeamsWorkspace = createTeamsWorkspace;
|
|
2193
|
+
exports.createWebSocketSignaling = createWebSocketSignaling;
|
|
2194
|
+
exports.detectOfficeKind = detectOfficeKind;
|
|
2195
|
+
exports.endpointUrl = endpointUrl;
|
|
2196
|
+
exports.filesOf = filesOf;
|
|
2197
|
+
exports.localServerConfig = localServerConfig;
|
|
2198
|
+
exports.parseServerConfig = parseServerConfig;
|
|
2199
|
+
exports.parseSignal = parseSignal;
|
|
2200
|
+
exports.peopleViews = peopleViews;
|
|
2201
|
+
exports.sanitizeAttachment = sanitizeAttachment;
|
|
2202
|
+
exports.sanitizeAttachments = sanitizeAttachments;
|
|
2203
|
+
exports.sanitizeChannelName = sanitizeChannelName;
|
|
2204
|
+
exports.sanitizeEmoji = sanitizeEmoji;
|
|
2205
|
+
exports.sanitizeMessageText = sanitizeMessageText;
|
|
2206
|
+
exports.searchMessages = searchMessages;
|
|
2207
|
+
exports.typingNames = typingNames;
|