Full message lifecycle used by InteractiveInkCanvas (src/canvas/variants/InteractiveInkCanvas.ts) via WebSocketClient (src/client/WebSocketClient.ts) against the MyScript Cloud recognition server. All message shapes come from TWebSocketClientMessageType in src/client/WebSocketClientMessage.ts.
Note: the
debug-websocketandrecognizer-systemproject skills describe a different, outdated protocol (src/recognizer/..., message types likestrokesAdded/sessionInitialized) — that path no longer exists in the codebase. This document reflects the currentsrc/client/WebSocketClient.tsimplementation; treat it as the source of truth.
sequenceDiagram
participant App as Application
participant Canvas as InteractiveInkCanvas
participant Client as WebSocketClient
participant Server as MyScript backend
%% 1. Connection + HMAC auth
App->>Canvas: Canvas.load(el, "INTERACTIVE_INK", options)
Canvas->>Client: new WebSocketClient(configuration.server)
Client->>Server: WS connect wss://.../offscreen?applicationKey=...
Client->>Server: {type: "authenticate", myscript-client-name, myscript-client-version}
Server-->>Client: {type: "hmacChallenge", hmacChallenge, iinkSessionId}
Client->>Client: computeHmac(hmacChallenge, applicationKey, hmacKey)
Client->>Server: {type: "hmac", hmac}
Server-->>Client: {type: "authenticated"}
%% 2. Session / content part init
Client->>Server: {type: "initSession", scaleX, scaleY, configuration}
Server-->>Client: {type: "sessionDescription", contentPartCount, iinkSessionId}
Client->>Server: {type: "newContentPart", contentType, mimeTypes}
Server-->>Client: {type: "newPart", id, idx}
Client->>Canvas: resolve initialized promise
Canvas-->>App: await editor.initialized.promise resolves
%% keepalive (parallel, independent worker)
loop every pingDelay (15s)
Client->>Server: {type: "ping"}
Server-->>Client: {type: "pong"}
end
%% 3. Stroke input + 4. real-time recognition
App->>Canvas: user writes (pointer events)
Canvas->>Canvas: writer / gesture / transform / erase managers build stroke ops
Canvas->>Client: addStrokes(strokes) / eraseStrokes / transform / replaceStrokes
Client->>Server: {type: "addStrokes", processGestures, strokes}
Server-->>Client: {type: "gestureDetected", ...} (if a gesture matched)
Server-->>Client: {type: "contentChanged", canUndo, canRedo, undoStackIndex}
Client->>Canvas: emit CONTENT_CHANGED (debounced 500ms)
Canvas->>Canvas: IISynchronizerManager.synchronize()
%% 5. Export / JIIX
Canvas->>Client: export(["application/vnd.myscript.jiix"])
Client->>Server: {type: "export", partId, mimeTypes}
Server-->>Client: {type: "exported", partId, exports}
Client->>Canvas: resolve export promise (JIIX parsed)
Canvas->>Canvas: IIJiixQueryManager rebuilds block index
%% math solver round trip (optional, on-demand)
Canvas->>Client: mathSolver request (get-variables / evaluate / ...)
Client->>Server: {type: "mathSolver", action, blockId, ...}
Server-->>Client: {type: "mathSolverResult", blockId, action, result}
%% 6. Undo / redo (client-authoritative stack, round-tripped to server)
App->>Canvas: undo()
Canvas->>Canvas: IIHistoryManager pops local stack, extractIIBackendChanges()
Canvas->>Client: undo(changes)
Client->>Server: {type: "undo", changes}
Server-->>Client: {type: "contentChanged", ...} (no dedicated ack)
%% 7. Session close
App->>Canvas: destroy()
Canvas->>Client: destroy()
Client->>Server: WS close frame (code 1000, "Client destroyed")
%% 8. Error handling / reconnection
Server--)Client: {type: "error", code, message} (e.g. no.activity, access.not.granted, session.too.old)
alt reconnect (autoReconnect / offline queue)
Client->>Client: enqueue offline messages (max 50)
loop every reconnectDelay (3s), up to maxReconnectAttempts (10)
Client->>Server: reconnect attempt (init())
end
Client->>Server: {type: "restoreSession", iinkSessionId} (on reconnect with known sessionId)
Server-->>Client: {type: "sessionDescription", ...}
Client->>Client: drain offline queue, replay queued messages
else give up
Client->>Canvas: emit CONNECTION_STATUS_CHANGED("error")
end
| Direction | type |
Purpose | Handler |
|---|---|---|---|
| C→S | authenticate |
Open handshake, client identity | WebSocketClient.openCallback() |
| S→C | hmacChallenge |
Challenge string to HMAC-sign | manageHMACChallenge() |
| C→S | hmac |
Computed HMAC response | — |
| S→C | authenticated |
Auth accepted | manageAuthenticated() |
| C→S | initSession / restoreSession |
Open new or resume existing session | manageAuthenticated() |
| S→C | sessionDescription |
Session id + content part count | manageSessionDescriptionMessage() |
| C→S | newContentPart / openContentPart |
Create or reopen a content part | — |
| S→C | newPart / partChanged |
Part ready, resolves initialized |
manageNewPartMessage() / managePartChangeMessage() |
| C→S | ping |
Keepalive (every 15s, worker-driven) | initPing() |
| S→C | pong |
Keepalive ack | messageCallback() |
| C→S | addStrokes |
New stroke batch (chunks of 1000) | addStrokes() |
| C→S | replaceStrokes / transform / eraseStrokes / contextlessGesture |
Edit operations | see WebSocketClient.ts |
| S→C | contentChanged |
Model changed server-side, triggers re-sync | manageContentChangedMessage() |
| S→C | gestureDetected / contextlessGesture |
Gesture classification result | manageGestureDetected() |
| C→S | export |
Request export in given mime types | export() |
| S→C | exported |
Export payload (e.g. JIIX) | manageExportMessage() |
| C→S | mathSolver |
Math variable/evaluation request | — |
| S→C | mathSolverResult |
Math solver response | manageMathSolverResult() |
| C→S | undo / redo |
Backend-side change replay for undo/redo | undo() / redo() |
| S→C | error |
Server error (no.activity, access.not.granted, session.too.old, restore.session.not.found) |
manageErrorMessage() |
| — | WS close frame | Clean/unclean disconnect | closeCallback(), ClientError.mapCloseCodeToMessage() |
Two independent mechanisms in WebSocketClient.ts:
- Legacy auto-reconnect in
send()— if the socket is closing/closed andconfiguration.server.websocket.autoReconnectis true (default), retriesinit()+ the send, up tomaxRetryCount(default 2). - Offline queue + background reconnect loop — if
offlineQueueEnabled(default true),addStrokes()queues locally (max 50) while disconnected; a timer retries everyreconnectDelay(default 3000ms) up tomaxReconnectAttempts(default 10), then drains the queue in order. Gives up and emitsCONNECTION_STATUS_CHANGED("error")if exhausted.
| Concern | File |
|---|---|
| Protocol state machine | src/client/WebSocketClient.ts |
| Message type enum + payload shapes | src/client/WebSocketClientMessage.ts |
| HMAC/applicationKey config | src/client/ServerConfiguration.ts |
| HMAC computation | src/utils/crypto.ts |
| Public event surface | src/client/ClientEvent.ts |
| Close-code → message mapping | src/client/ClientError.ts |
| Editor-side wiring (init/undo/redo/export/destroy) | src/canvas/variants/InteractiveInkCanvas.ts |
Debounced JIIX re-sync after contentChanged |
src/manager/interactive/IISynchronizerManager.ts |