@sudo-ping-pong/prism-expo-client 0.1.1

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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -0
  3. package/dist/buffer.d.ts +8 -0
  4. package/dist/buffer.d.ts.map +1 -0
  5. package/dist/buffer.js +19 -0
  6. package/dist/buffer.js.map +1 -0
  7. package/dist/capture.d.ts +5 -0
  8. package/dist/capture.d.ts.map +1 -0
  9. package/dist/capture.js +36 -0
  10. package/dist/capture.js.map +1 -0
  11. package/dist/command-handler.d.ts +2 -0
  12. package/dist/command-handler.d.ts.map +1 -0
  13. package/dist/command-handler.js +14 -0
  14. package/dist/command-handler.js.map +1 -0
  15. package/dist/config.d.ts +22 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +15 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/index.d.ts +38 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +103 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/middleware/redux.d.ts +28 -0
  24. package/dist/middleware/redux.d.ts.map +1 -0
  25. package/dist/middleware/redux.js +94 -0
  26. package/dist/middleware/redux.js.map +1 -0
  27. package/dist/middleware/zustand.d.ts +11 -0
  28. package/dist/middleware/zustand.d.ts.map +1 -0
  29. package/dist/middleware/zustand.js +37 -0
  30. package/dist/middleware/zustand.js.map +1 -0
  31. package/dist/patches/axios.d.ts +4 -0
  32. package/dist/patches/axios.d.ts.map +1 -0
  33. package/dist/patches/axios.js +140 -0
  34. package/dist/patches/axios.js.map +1 -0
  35. package/dist/patches/capture-guard.d.ts +5 -0
  36. package/dist/patches/capture-guard.d.ts.map +1 -0
  37. package/dist/patches/capture-guard.js +24 -0
  38. package/dist/patches/capture-guard.js.map +1 -0
  39. package/dist/patches/console.d.ts +2 -0
  40. package/dist/patches/console.d.ts.map +1 -0
  41. package/dist/patches/console.js +21 -0
  42. package/dist/patches/console.js.map +1 -0
  43. package/dist/patches/fetch.d.ts +4 -0
  44. package/dist/patches/fetch.d.ts.map +1 -0
  45. package/dist/patches/fetch.js +106 -0
  46. package/dist/patches/fetch.js.map +1 -0
  47. package/dist/patches/xhr.d.ts +2 -0
  48. package/dist/patches/xhr.d.ts.map +1 -0
  49. package/dist/patches/xhr.js +74 -0
  50. package/dist/patches/xhr.js.map +1 -0
  51. package/dist/profiler.d.ts +9 -0
  52. package/dist/profiler.d.ts.map +1 -0
  53. package/dist/profiler.js +19 -0
  54. package/dist/profiler.js.map +1 -0
  55. package/dist/transport-registry.d.ts +8 -0
  56. package/dist/transport-registry.d.ts.map +1 -0
  57. package/dist/transport-registry.js +11 -0
  58. package/dist/transport-registry.js.map +1 -0
  59. package/dist/transport.d.ts +38 -0
  60. package/dist/transport.d.ts.map +1 -0
  61. package/dist/transport.js +153 -0
  62. package/dist/transport.js.map +1 -0
  63. package/package.json +111 -0
  64. package/src/buffer.ts +23 -0
  65. package/src/capture.ts +40 -0
  66. package/src/command-handler.ts +15 -0
  67. package/src/config.ts +29 -0
  68. package/src/env.d.ts +1 -0
  69. package/src/index.ts +148 -0
  70. package/src/middleware/redux.ts +126 -0
  71. package/src/middleware/zustand.ts +58 -0
  72. package/src/patches/axios.ts +182 -0
  73. package/src/patches/capture-guard.ts +24 -0
  74. package/src/patches/console.ts +27 -0
  75. package/src/patches/fetch.ts +118 -0
  76. package/src/patches/xhr.ts +103 -0
  77. package/src/profiler.tsx +36 -0
  78. package/src/transport-registry.ts +20 -0
  79. package/src/transport.ts +198 -0
@@ -0,0 +1,153 @@
1
+ import { isPrismCommand } from '@sudo-ping-pong/prism-protocol';
2
+ import { EventBuffer } from './buffer';
3
+ import { handlePrismCommand } from './command-handler';
4
+ const WS_OPEN = 1;
5
+ const MAX_BACKOFF_MS = 30_000;
6
+ export class PrismTransport {
7
+ ws = null;
8
+ buffer = new EventBuffer();
9
+ reconnectTimer = null;
10
+ options;
11
+ connected = false;
12
+ destroyed = false;
13
+ authFailed = false;
14
+ reconnectAttempts = 0;
15
+ constructor(options) {
16
+ this.options = options;
17
+ }
18
+ connect() {
19
+ if (this.destroyed || this.authFailed)
20
+ return;
21
+ const url = `ws://${this.options.host}:${this.options.port}`;
22
+ const WS = getWebSocketConstructor();
23
+ if (!WS) {
24
+ this.options.onError?.({
25
+ type: 'unavailable',
26
+ message: 'WebSocket is not available in this environment',
27
+ });
28
+ return;
29
+ }
30
+ if (this.ws) {
31
+ this.ws.onclose = null;
32
+ this.ws.onerror = null;
33
+ this.ws.onmessage = null;
34
+ this.ws.close();
35
+ this.ws = null;
36
+ }
37
+ this.ws = new WS(url);
38
+ this.ws.onopen = () => {
39
+ this.reconnectAttempts = 0;
40
+ this.ws?.send(JSON.stringify({
41
+ type: 'handshake',
42
+ role: 'sdk',
43
+ }));
44
+ };
45
+ this.ws.onmessage = (ev) => {
46
+ try {
47
+ const msg = JSON.parse(ev.data);
48
+ if (typeof msg === 'object' &&
49
+ msg !== null &&
50
+ 'type' in msg &&
51
+ msg.type === 'handshake_ack') {
52
+ const ack = msg;
53
+ if (ack.success) {
54
+ this.connected = true;
55
+ this.options.onConnect?.();
56
+ this.flushBuffer();
57
+ }
58
+ else {
59
+ this.authFailed = true;
60
+ const message = ack.message ?? 'Handshake rejected by server';
61
+ this.options.onError?.({ type: 'auth', message });
62
+ this.ws?.close();
63
+ }
64
+ return;
65
+ }
66
+ if (this.connected && isPrismCommand(msg)) {
67
+ handlePrismCommand(msg);
68
+ }
69
+ }
70
+ catch {
71
+ // ignore malformed messages
72
+ }
73
+ };
74
+ this.ws.onclose = () => {
75
+ this.connected = false;
76
+ this.ws = null;
77
+ if (this.destroyed || this.authFailed)
78
+ return;
79
+ this.options.onDisconnect?.();
80
+ this.scheduleReconnect();
81
+ };
82
+ this.ws.onerror = () => {
83
+ this.connected = false;
84
+ this.options.onError?.({
85
+ type: 'network',
86
+ message: `Cannot reach Prism server at ${this.options.host}:${this.options.port}`,
87
+ });
88
+ };
89
+ }
90
+ send(envelope) {
91
+ queueMicrotask(() => {
92
+ if (this.connected && this.ws?.readyState === WS_OPEN) {
93
+ try {
94
+ this.ws.send(JSON.stringify(envelope));
95
+ }
96
+ catch {
97
+ this.buffer.push(envelope);
98
+ }
99
+ }
100
+ else {
101
+ this.buffer.push(envelope);
102
+ }
103
+ });
104
+ }
105
+ disconnect() {
106
+ this.destroyed = true;
107
+ if (this.reconnectTimer) {
108
+ clearTimeout(this.reconnectTimer);
109
+ this.reconnectTimer = null;
110
+ }
111
+ if (this.ws) {
112
+ this.ws.onclose = null;
113
+ this.ws.onerror = null;
114
+ this.ws.close();
115
+ this.ws = null;
116
+ }
117
+ this.connected = false;
118
+ }
119
+ resetAuthFailure() {
120
+ this.authFailed = false;
121
+ this.destroyed = false;
122
+ this.reconnectAttempts = 0;
123
+ }
124
+ get isConnected() {
125
+ return this.connected;
126
+ }
127
+ get hasAuthFailed() {
128
+ return this.authFailed;
129
+ }
130
+ flushBuffer() {
131
+ const pending = this.buffer.drain();
132
+ for (const envelope of pending) {
133
+ this.send(envelope);
134
+ }
135
+ }
136
+ scheduleReconnect() {
137
+ if (this.destroyed || this.authFailed || this.reconnectTimer)
138
+ return;
139
+ const delay = Math.min(3000 * 2 ** this.reconnectAttempts, MAX_BACKOFF_MS);
140
+ this.reconnectAttempts++;
141
+ this.reconnectTimer = setTimeout(() => {
142
+ this.reconnectTimer = null;
143
+ this.connect();
144
+ }, delay);
145
+ }
146
+ }
147
+ function getWebSocketConstructor() {
148
+ if (typeof globalThis.WebSocket !== 'undefined') {
149
+ return globalThis.WebSocket;
150
+ }
151
+ return null;
152
+ }
153
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAyBvD,MAAM,OAAO,GAAG,CAAC,CAAC;AAClB,MAAM,cAAc,GAAG,MAAM,CAAC;AAE9B,MAAM,OAAO,cAAc;IACjB,EAAE,GAAyB,IAAI,CAAC;IACvB,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;IACpC,cAAc,GAAyC,IAAI,CAAC;IACnD,OAAO,CAAmB;IACnC,SAAS,GAAG,KAAK,CAAC;IAClB,SAAS,GAAG,KAAK,CAAC;IAClB,UAAU,GAAG,KAAK,CAAC;IACnB,iBAAiB,GAAG,CAAC,CAAC;IAE9B,YAAY,OAAyB;QACnC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAE9C,MAAM,GAAG,GAAG,QAAQ,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAC7D,MAAM,EAAE,GAAG,uBAAuB,EAAE,CAAC;QACrC,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;gBACrB,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,gDAAgD;aAC1D,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;YACZ,IAAI,CAAC,EAAE,CAAC,OAAO,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,EAAE,CAAC,OAAO,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,EAAE,CAAC,SAAS,GAAG,IAAI,CAAC;YACzB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;QACjB,CAAC;QAED,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,GAAG,CAAkB,CAAC;QAEvC,IAAI,CAAC,EAAE,CAAC,MAAM,GAAG,GAAG,EAAE;YACpB,IAAI,CAAC,iBAAiB,GAAG,CAAC,CAAC;YAC3B,IAAI,CAAC,EAAE,EAAE,IAAI,CACX,IAAI,CAAC,SAAS,CAAC;gBACb,IAAI,EAAE,WAAW;gBACjB,IAAI,EAAE,KAAK;aACZ,CAAC,CACH,CAAC;QACJ,CAAC,CAAC;QAEF,IAAI,CAAC,EAAE,CAAC,SAAS,GAAG,CAAC,EAAE,EAAE,EAAE;YACzB,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAgC,CAAC;gBAE/D,IACE,OAAO,GAAG,KAAK,QAAQ;oBACvB,GAAG,KAAK,IAAI;oBACZ,MAAM,IAAI,GAAG;oBACZ,GAAyB,CAAC,IAAI,KAAK,eAAe,EACnD,CAAC;oBACD,MAAM,GAAG,GAAG,GAAwB,CAAC;oBACrC,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;wBAChB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;wBACtB,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;wBAC3B,IAAI,CAAC,WAAW,EAAE,CAAC;oBACrB,CAAC;yBAAM,CAAC;wBACN,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;wBACvB,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,8BAA8B,CAAC;wBAC9D,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;wBAClD,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;oBACnB,CAAC;oBACD,OAAO;gBACT,CAAC;gBAED,IAAI,IAAI,CAAC,SAAS,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;oBAC1C,kBAAkB,CAAC,GAAG,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,4BAA4B;YAC9B,CAAC;QACH,CAAC,CAAC;QAEF,IAAI,CAAC,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;YACrB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACvB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;YAEf,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU;gBAAE,OAAO;YAE9C,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;YAC9B,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC3B,CAAC,CAAC;QAEF,IAAI,CAAC,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;YACrB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACvB,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;gBACrB,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,gCAAgC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;aAClF,CAAC,CAAC;QACL,CAAC,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAuB;QAC1B,cAAc,CAAC,GAAG,EAAE;YAClB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,EAAE,EAAE,UAAU,KAAK,OAAO,EAAE,CAAC;gBACtD,IAAI,CAAC;oBACH,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACzC,CAAC;gBAAC,MAAM,CAAC;oBACP,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC7B,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,UAAU;QACR,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAClC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;YACZ,IAAI,CAAC,EAAE,CAAC,OAAO,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,EAAE,CAAC,OAAO,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACzB,CAAC;IAED,gBAAgB;QACd,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,iBAAiB,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAEO,WAAW;QACjB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpC,KAAK,MAAM,QAAQ,IAAI,OAAO,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAEO,iBAAiB;QACvB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,cAAc;YAAE,OAAO;QAErE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;QAC3E,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAEzB,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,GAAG,EAAE;YACpC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC;CACF;AAED,SAAS,uBAAuB;IAC9B,IAAI,OAAO,UAAU,CAAC,SAAS,KAAK,WAAW,EAAE,CAAC;QAChD,OAAO,UAAU,CAAC,SAA0D,CAAC;IAC/E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json ADDED
@@ -0,0 +1,111 @@
1
+ {
2
+ "name": "@sudo-ping-pong/prism-expo-client",
3
+ "version": "0.1.1",
4
+ "description": "Client SDK for Prism DevTools — network, log, and state interception for Expo and React Native",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "react-native": "./src/index.ts",
10
+ "exports": {
11
+ ".": {
12
+ "react-native": "./src/index.ts",
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./zustand": {
18
+ "react-native": "./src/middleware/zustand.ts",
19
+ "types": "./dist/middleware/zustand.d.ts",
20
+ "import": "./dist/middleware/zustand.js",
21
+ "default": "./dist/middleware/zustand.js"
22
+ },
23
+ "./profiler": {
24
+ "react-native": "./src/profiler.tsx",
25
+ "types": "./dist/profiler.d.ts",
26
+ "import": "./dist/profiler.js",
27
+ "default": "./dist/profiler.js"
28
+ },
29
+ "./capture": {
30
+ "types": "./dist/capture.d.ts",
31
+ "import": "./dist/capture.js",
32
+ "default": "./dist/capture.js"
33
+ },
34
+ "./transport-registry": {
35
+ "types": "./dist/transport-registry.d.ts",
36
+ "import": "./dist/transport-registry.js",
37
+ "default": "./dist/transport-registry.js"
38
+ },
39
+ "./redux": {
40
+ "react-native": "./src/middleware/redux.ts",
41
+ "types": "./dist/middleware/redux.d.ts",
42
+ "import": "./dist/middleware/redux.js",
43
+ "default": "./dist/middleware/redux.js"
44
+ },
45
+ "./axios": {
46
+ "types": "./dist/patches/axios.d.ts",
47
+ "import": "./dist/patches/axios.js",
48
+ "default": "./dist/patches/axios.js"
49
+ }
50
+ },
51
+ "files": ["dist", "src"],
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/sudo-ping-pong/prism-expo-client.git"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/sudo-ping-pong/prism-expo-client/issues"
58
+ },
59
+ "homepage": "https://github.com/sudo-ping-pong/prism-expo-client#readme",
60
+ "keywords": [
61
+ "prism",
62
+ "devtools",
63
+ "expo",
64
+ "react-native",
65
+ "debugging",
66
+ "redux",
67
+ "zustand",
68
+ "websocket"
69
+ ],
70
+ "publishConfig": {
71
+ "access": "public",
72
+ "registry": "https://registry.npmjs.org/"
73
+ },
74
+ "scripts": {
75
+ "build": "tsc",
76
+ "typecheck": "tsc --noEmit",
77
+ "prepublishOnly": "pnpm run build"
78
+ },
79
+ "dependencies": {
80
+ "@sudo-ping-pong/prism-protocol": "^0.1.1"
81
+ },
82
+ "peerDependencies": {
83
+ "react": ">=18.0.0",
84
+ "redux": ">=4.0.0",
85
+ "zustand": ">=4.0.0",
86
+ "axios": ">=1.0.0"
87
+ },
88
+ "peerDependenciesMeta": {
89
+ "react": {
90
+ "optional": true
91
+ },
92
+ "redux": {
93
+ "optional": true
94
+ },
95
+ "zustand": {
96
+ "optional": true
97
+ },
98
+ "axios": {
99
+ "optional": true
100
+ }
101
+ },
102
+ "devDependencies": {
103
+ "@sudo-ping-pong/prism-protocol": "^0.1.1",
104
+ "@types/react": "^19.0.2",
105
+ "axios": "^1.7.9",
106
+ "react": "^19.0.0",
107
+ "redux": "^5.0.1",
108
+ "typescript": "^5.7.3",
109
+ "zustand": "^5.0.3"
110
+ }
111
+ }
package/src/buffer.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { PrismEnvelope } from '@sudo-ping-pong/prism-protocol';
2
+ import { PRISM_MAX_BUFFER_SIZE } from '@sudo-ping-pong/prism-protocol';
3
+
4
+ export class EventBuffer {
5
+ private buffer: PrismEnvelope[] = [];
6
+
7
+ push(envelope: PrismEnvelope): void {
8
+ if (this.buffer.length >= PRISM_MAX_BUFFER_SIZE) {
9
+ this.buffer.shift();
10
+ }
11
+ this.buffer.push(envelope);
12
+ }
13
+
14
+ drain(): PrismEnvelope[] {
15
+ const items = [...this.buffer];
16
+ this.buffer = [];
17
+ return items;
18
+ }
19
+
20
+ get size(): number {
21
+ return this.buffer.length;
22
+ }
23
+ }
package/src/capture.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { createEnvelope } from '@sudo-ping-pong/prism-protocol';
2
+ import type { LogLevel, LogPayload, NetworkPayload, StatePayload } from '@sudo-ping-pong/prism-protocol';
3
+ import { sendToPrism } from './transport-registry';
4
+
5
+ export function captureLogEvent(level: LogLevel, args: unknown[]): void {
6
+ const payload: LogPayload = {
7
+ level,
8
+ args: serializeArgs(args),
9
+ stack: level === 'error' ? captureStack() : undefined,
10
+ };
11
+ sendToPrism(createEnvelope('LOG', payload));
12
+ }
13
+
14
+ export function captureNetworkEvent(payload: NetworkPayload): void {
15
+ sendToPrism(createEnvelope('NETWORK', payload));
16
+ }
17
+
18
+ export function captureStateEvent(payload: StatePayload): void {
19
+ sendToPrism(createEnvelope('STATE', payload));
20
+ }
21
+
22
+ function serializeArgs(args: unknown[]): unknown[] {
23
+ return args.map((arg) => {
24
+ if (arg instanceof Error) {
25
+ return { name: arg.name, message: arg.message, stack: arg.stack };
26
+ }
27
+ try {
28
+ JSON.stringify(arg);
29
+ return arg;
30
+ } catch {
31
+ return String(arg);
32
+ }
33
+ });
34
+ }
35
+
36
+ function captureStack(): string | undefined {
37
+ const err = new Error();
38
+ const lines = err.stack?.split('\n').slice(2);
39
+ return lines?.join('\n');
40
+ }
@@ -0,0 +1,15 @@
1
+ import { isPrismCommand } from '@sudo-ping-pong/prism-protocol';
2
+ import { dispatchPrismReduxCommand } from './middleware/redux';
3
+
4
+ export function handlePrismCommand(raw: unknown): void {
5
+ if (!isPrismCommand(raw)) return;
6
+
7
+ if (raw.command === 'dispatch') {
8
+ dispatchPrismReduxCommand(raw.storeId, 'dispatch', raw.action);
9
+ return;
10
+ }
11
+
12
+ if (raw.command === 'replace_state') {
13
+ dispatchPrismReduxCommand(raw.storeId, 'replace_state', raw.state);
14
+ }
15
+ }
package/src/config.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { PRISM_DEFAULT_PORT } from '@sudo-ping-pong/prism-protocol';
2
+ import type { PrismTransportError } from './transport';
3
+
4
+ export interface PrismLocalConfigOptions {
5
+ port?: number;
6
+ network?: boolean;
7
+ logs?: boolean;
8
+ forceEnable?: boolean;
9
+ onConnect?: () => void;
10
+ onDisconnect?: () => void;
11
+ onError?: (error: PrismTransportError) => void;
12
+ }
13
+
14
+ /** Build SDK config for a host (defaults to port 8080). */
15
+ export function createPrismLocalConfig(
16
+ host: string,
17
+ overrides?: PrismLocalConfigOptions,
18
+ ) {
19
+ return {
20
+ host,
21
+ port: overrides?.port ?? PRISM_DEFAULT_PORT,
22
+ network: overrides?.network,
23
+ logs: overrides?.logs,
24
+ forceEnable: overrides?.forceEnable,
25
+ onConnect: overrides?.onConnect,
26
+ onDisconnect: overrides?.onDisconnect,
27
+ onError: overrides?.onError,
28
+ };
29
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare const process: { env?: { NODE_ENV?: string } } | undefined;
package/src/index.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { PrismTransport } from './transport';
2
+ import { installFetchPatch } from './patches/fetch';
3
+ import { installXhrPatch } from './patches/xhr';
4
+ import { installAxiosPatch } from './patches/axios';
5
+ import { installConsolePatch } from './patches/console';
6
+ import { setActiveTransport } from './transport-registry';
7
+ import { createPrismLocalConfig } from './config';
8
+ import type { PrismTransportError } from './transport';
9
+
10
+ export interface PrismConfig {
11
+ /** Machine running Prism (LAN IP or localhost) */
12
+ host: string;
13
+ /** WebSocket port (default 8080) */
14
+ port?: number;
15
+ /** Intercept fetch (default true) */
16
+ network?: boolean;
17
+ /** Intercept console.log/warn/error (default true) */
18
+ logs?: boolean;
19
+ /** Force enable in production (default: only __DEV__) */
20
+ forceEnable?: boolean;
21
+ onConnect?: () => void;
22
+ onDisconnect?: () => void;
23
+ onError?: (error: PrismTransportError) => void;
24
+ }
25
+
26
+ let initialized = false;
27
+ let transport: PrismTransport | null = null;
28
+ let patchesInstalled = false;
29
+ let unsubs: Array<() => void> = [];
30
+
31
+ function startTransport(config: PrismConfig): void {
32
+ transport?.disconnect();
33
+
34
+ transport = new PrismTransport({
35
+ host: config.host,
36
+ port: config.port ?? 8080,
37
+ onConnect: config.onConnect ?? (() => logDev('[Prism] Connected to DevTools')),
38
+ onDisconnect: config.onDisconnect ?? (() => logDev('[Prism] Disconnected — retrying…')),
39
+ onError:
40
+ config.onError ??
41
+ ((error) => {
42
+ if (error.type === 'auth') {
43
+ console.warn(`[Prism] ${error.message}`);
44
+ } else if (error.type === 'network') {
45
+ logDev(`[Prism] ${error.message}`);
46
+ } else {
47
+ console.warn(`[Prism] ${error.message}`);
48
+ }
49
+ }),
50
+ });
51
+
52
+ setActiveTransport(transport);
53
+ transport.resetAuthFailure();
54
+ transport.connect();
55
+ }
56
+
57
+ function installPatches(config: PrismConfig): void {
58
+ if (patchesInstalled || !transport) return;
59
+
60
+ if (config.network !== false) {
61
+ unsubs.push(installFetchPatch());
62
+ unsubs.push(installXhrPatch());
63
+ unsubs.push(installAxiosPatch());
64
+ }
65
+ if (config.logs !== false) {
66
+ unsubs.push(installConsolePatch());
67
+ }
68
+ patchesInstalled = true;
69
+ }
70
+
71
+ export function initPrism(config: PrismConfig): void {
72
+ const isDev =
73
+ typeof __DEV__ !== 'undefined'
74
+ ? __DEV__
75
+ : typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';
76
+
77
+ if (!config.forceEnable && !isDev) return;
78
+ if (initialized) return;
79
+
80
+ initialized = true;
81
+ startTransport(config);
82
+ installPatches(config);
83
+ }
84
+
85
+ /** One-liner init — pass your machine's LAN IP (or Expo dev host). */
86
+ export function initPrismLocal(host: string, overrides?: Partial<PrismConfig>): void {
87
+ initPrism(createPrismLocalConfig(host, overrides));
88
+ }
89
+
90
+ /** Reconnect with updated host/port without reinstalling patches. */
91
+ export function reconnectPrism(config: PrismConfig): void {
92
+ const isDev =
93
+ typeof __DEV__ !== 'undefined'
94
+ ? __DEV__
95
+ : typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';
96
+
97
+ if (!config.forceEnable && !isDev) return;
98
+
99
+ if (!initialized) {
100
+ initPrism(config);
101
+ return;
102
+ }
103
+
104
+ startTransport(config);
105
+ }
106
+
107
+ export function destroyPrism(): void {
108
+ transport?.disconnect();
109
+ setActiveTransport(null);
110
+ transport = null;
111
+ initialized = false;
112
+
113
+ for (const unsub of unsubs) unsub();
114
+ unsubs = [];
115
+ patchesInstalled = false;
116
+ }
117
+
118
+ export function getPrismTransport(): PrismTransport | null {
119
+ return transport;
120
+ }
121
+
122
+ export { PrismTransport } from './transport';
123
+ export type { TransportOptions, PrismTransportError } from './transport';
124
+
125
+ export { prismZustandMiddleware } from './middleware/zustand';
126
+ export type { PrismZustandOptions } from './middleware/zustand';
127
+ export { PrismProfiler } from './profiler';
128
+ export type { PrismProfilerProps } from './profiler';
129
+ export {
130
+ prismReduxMiddleware,
131
+ withPrismStateReplay,
132
+ registerPrismReduxStore,
133
+ PRISM_REPLACE_STATE,
134
+ } from './middleware/redux';
135
+ export type { PrismReduxOptions, PrismReplaceStateAction } from './middleware/redux';
136
+ export { parsePrismConnectUri } from '@sudo-ping-pong/prism-protocol';
137
+ export type { PrismConnectConfig, PrismCommand, StateSource } from '@sudo-ping-pong/prism-protocol';
138
+ export { installAxiosPatch } from './patches/axios';
139
+ export { createPrismLocalConfig } from './config';
140
+ export type { PrismLocalConfigOptions } from './config';
141
+
142
+ declare const __DEV__: boolean | undefined;
143
+
144
+ function logDev(message: string): void {
145
+ if (typeof __DEV__ !== 'undefined' ? __DEV__ : true) {
146
+ console.log(message);
147
+ }
148
+ }
@@ -0,0 +1,126 @@
1
+ import type { Store, Action, Reducer } from 'redux';
2
+ import type { StatePayload } from '@sudo-ping-pong/prism-protocol';
3
+ import { captureStateEvent } from '../capture';
4
+
5
+ /** Dispatched by Prism to replace the entire Redux state (time-travel jump). */
6
+ export const PRISM_REPLACE_STATE = '@@prism/REPLACE_STATE';
7
+
8
+ export interface PrismReplaceStateAction<S = unknown> extends Action<typeof PRISM_REPLACE_STATE> {
9
+ payload: S;
10
+ }
11
+
12
+ export interface PrismReduxOptions {
13
+ storeId?: string;
14
+ /** Redux action types to skip, e.g. ['@@INIT'] */
15
+ ignoredActions?: string[];
16
+ }
17
+
18
+ const reduxStores = new Map<string, Store>();
19
+
20
+ export function registerPrismReduxStore(store: Store, storeId = 'redux'): () => void {
21
+ reduxStores.set(storeId, store);
22
+ return () => reduxStores.delete(storeId);
23
+ }
24
+
25
+ export function getPrismReduxStore(storeId: string): Store | undefined {
26
+ return reduxStores.get(storeId);
27
+ }
28
+
29
+ /**
30
+ * Wrap a root reducer so Prism can jump to a historical state snapshot.
31
+ *
32
+ * ```ts
33
+ * const store = configureStore({
34
+ * reducer: withPrismStateReplay(rootReducer),
35
+ * middleware: (gDM) => gDM().concat(prismReduxMiddleware('app')),
36
+ * });
37
+ * registerPrismReduxStore(store, 'app');
38
+ * ```
39
+ */
40
+ export function withPrismStateReplay<S>(reducer: Reducer<S>): Reducer<S> {
41
+ return (state, action) => {
42
+ if (action.type === PRISM_REPLACE_STATE) {
43
+ return (action as unknown as PrismReplaceStateAction<S>).payload;
44
+ }
45
+ return reducer(state, action);
46
+ };
47
+ }
48
+
49
+ const DEFAULT_IGNORED = ['@@INIT', '@@redux/INIT'];
50
+
51
+ export function prismReduxMiddleware(
52
+ storeIdOrOptions?: string | PrismReduxOptions,
53
+ ): import('redux').Middleware {
54
+ const opts: PrismReduxOptions =
55
+ typeof storeIdOrOptions === 'string'
56
+ ? { storeId: storeIdOrOptions }
57
+ : { storeId: 'redux', ...storeIdOrOptions };
58
+
59
+ const storeId = opts.storeId ?? 'redux';
60
+ const ignored = new Set([...DEFAULT_IGNORED, ...(opts.ignoredActions ?? [])]);
61
+
62
+ return (storeApi) => {
63
+ reduxStores.set(storeId, storeApi as unknown as Store);
64
+
65
+ return (next) => (action) => {
66
+ const prevState = storeApi.getState();
67
+ const result = next(action);
68
+ const nextState = storeApi.getState();
69
+
70
+ const actionType = (action as Action).type ?? 'unknown';
71
+ if (ignored.has(actionType) || actionType === PRISM_REPLACE_STATE) {
72
+ return result;
73
+ }
74
+
75
+ const payload: StatePayload = {
76
+ actionLabel: actionType,
77
+ payload: serializeAction(action),
78
+ prevState: cloneState(prevState),
79
+ nextState: cloneState(nextState),
80
+ storeId,
81
+ source: 'redux',
82
+ };
83
+
84
+ captureStateEvent(payload);
85
+ return result;
86
+ };
87
+ };
88
+ }
89
+
90
+ export function dispatchPrismReduxCommand(
91
+ storeId: string,
92
+ command: 'dispatch' | 'replace_state',
93
+ data: unknown,
94
+ ): boolean {
95
+ const store = reduxStores.get(storeId);
96
+ if (!store) return false;
97
+
98
+ if (command === 'dispatch') {
99
+ store.dispatch(data as Action);
100
+ return true;
101
+ }
102
+
103
+ store.dispatch({
104
+ type: PRISM_REPLACE_STATE,
105
+ payload: data,
106
+ } as unknown as Action);
107
+ return true;
108
+ }
109
+
110
+ function serializeAction(action: unknown): unknown {
111
+ if (typeof action !== 'object' || action === null) return action;
112
+ try {
113
+ JSON.stringify(action);
114
+ return action;
115
+ } catch {
116
+ return { type: (action as Action).type, note: '(non-serializable action)' };
117
+ }
118
+ }
119
+
120
+ function cloneState<T>(value: T): T {
121
+ try {
122
+ return JSON.parse(JSON.stringify(value)) as T;
123
+ } catch {
124
+ return value;
125
+ }
126
+ }