orez 0.2.17 → 0.2.19
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/index.d.ts.map +1 -1
- package/dist/index.js +45 -3
- package/dist/index.js.map +1 -1
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +18 -17
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +86 -8
- package/dist/replication/handler.js.map +1 -1
- package/dist/worker/shims/ws-browser.d.ts +1 -1
- package/dist/worker/shims/ws-browser.d.ts.map +1 -1
- package/dist/worker/shims/ws-browser.js +29 -5
- package/dist/worker/shims/ws-browser.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +49 -3
- package/src/replication/change-tracker.test.ts +42 -4
- package/src/replication/change-tracker.ts +20 -20
- package/src/replication/handler.ts +121 -9
- package/src/replication/zero-compat.test.ts +92 -10
- package/src/worker/shims/ws-browser.test.ts +21 -0
- package/src/worker/shims/ws-browser.ts +35 -6
|
@@ -134,6 +134,7 @@ export function resetReplicationState(): void {
|
|
|
134
134
|
cachedTableKeyColumns = null
|
|
135
135
|
cachedExcludedColumns = null
|
|
136
136
|
cachedColumnTypeOids = null
|
|
137
|
+
cachedColumns.clear()
|
|
137
138
|
}
|
|
138
139
|
function nextLsn(): bigint {
|
|
139
140
|
const floor = lsnFloorFromTime()
|
|
@@ -539,8 +540,8 @@ export async function handleStartReplication(
|
|
|
539
540
|
if (shardClientSchemas.length > 0) {
|
|
540
541
|
const shardTables = await db.query<{ schemaname: string; tablename: string }>(
|
|
541
542
|
`SELECT schemaname, tablename FROM pg_tables
|
|
542
|
-
WHERE schemaname = ANY($1) AND tablename =
|
|
543
|
-
[shardClientSchemas]
|
|
543
|
+
WHERE schemaname = ANY($1) AND tablename = ANY($2)`,
|
|
544
|
+
[shardClientSchemas, ['clients', 'mutations']]
|
|
544
545
|
)
|
|
545
546
|
for (const { schemaname, tablename } of shardTables.rows) {
|
|
546
547
|
const qs = '"' + schemaname.replace(/"/g, '""') + '"'
|
|
@@ -578,8 +579,9 @@ export async function handleStartReplication(
|
|
|
578
579
|
table_name: string
|
|
579
580
|
column_name: string
|
|
580
581
|
data_type: string | null
|
|
582
|
+
ordinal_position: number
|
|
581
583
|
}>(
|
|
582
|
-
`SELECT 'pk' AS kind, tc.table_schema, tc.table_name, kcu.column_name, NULL AS data_type
|
|
584
|
+
`SELECT 'pk' AS kind, tc.table_schema, tc.table_name, kcu.column_name, NULL AS data_type, kcu.ordinal_position
|
|
583
585
|
FROM information_schema.table_constraints tc
|
|
584
586
|
JOIN information_schema.key_column_usage kcu
|
|
585
587
|
ON tc.constraint_name = kcu.constraint_name
|
|
@@ -587,9 +589,10 @@ export async function handleStartReplication(
|
|
|
587
589
|
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
588
590
|
AND tc.table_schema = ANY($1)
|
|
589
591
|
UNION ALL
|
|
590
|
-
SELECT 'col' AS kind, table_schema, table_name, column_name, data_type
|
|
592
|
+
SELECT 'col' AS kind, table_schema, table_name, column_name, data_type, ordinal_position
|
|
591
593
|
FROM information_schema.columns
|
|
592
|
-
WHERE table_schema = ANY($1)
|
|
594
|
+
WHERE table_schema = ANY($1)
|
|
595
|
+
ORDER BY table_schema, table_name, kind, ordinal_position`,
|
|
593
596
|
[relevantSchemas]
|
|
594
597
|
)
|
|
595
598
|
|
|
@@ -834,9 +837,9 @@ export async function handleStartReplication(
|
|
|
834
837
|
`found ${changes.length} changes [${tableSummary}] (wm ${lastWatermark}→${changes[changes.length - 1].watermark}) query=${queryMs.toFixed(1)}ms signal→query=${signalToQueryMs}ms`
|
|
835
838
|
)
|
|
836
839
|
// filter out shard tables that zero-cache doesn't expect.
|
|
837
|
-
//
|
|
838
|
-
// other shard tables (replicas
|
|
839
|
-
//
|
|
840
|
+
// `clients` advances lmid; `mutations` carries mutation results.
|
|
841
|
+
// other shard tables (e.g. replicas) crash zero-cache with
|
|
842
|
+
// "Unknown table" in the change processor.
|
|
840
843
|
const batchEnd = changes[changes.length - 1].watermark
|
|
841
844
|
const preFilterCount = changes.length
|
|
842
845
|
changes = changes.filter((c) => {
|
|
@@ -845,7 +848,7 @@ export async function handleStartReplication(
|
|
|
845
848
|
const schema = c.table_name.substring(0, dot)
|
|
846
849
|
if (schema === 'public') return true
|
|
847
850
|
const table = c.table_name.substring(dot + 1)
|
|
848
|
-
return table === 'clients'
|
|
851
|
+
return table === 'clients' || table === 'mutations'
|
|
849
852
|
})
|
|
850
853
|
log.debug.repl(`filter: ${preFilterCount} → ${changes.length} changes`)
|
|
851
854
|
|
|
@@ -859,6 +862,15 @@ export async function handleStartReplication(
|
|
|
859
862
|
continue
|
|
860
863
|
}
|
|
861
864
|
|
|
865
|
+
await ensureMetadataForChangedTables(
|
|
866
|
+
db,
|
|
867
|
+
mutex,
|
|
868
|
+
changes,
|
|
869
|
+
tableKeyColumns,
|
|
870
|
+
excludedColumns,
|
|
871
|
+
columnTypeOids
|
|
872
|
+
)
|
|
873
|
+
|
|
862
874
|
log.debug.repl(`streaming ${changes.length} changes to writer`)
|
|
863
875
|
await streamChanges(
|
|
864
876
|
changes,
|
|
@@ -930,6 +942,106 @@ export async function handleStartReplication(
|
|
|
930
942
|
log.repl('poll loop exited')
|
|
931
943
|
}
|
|
932
944
|
|
|
945
|
+
async function ensureMetadataForChangedTables(
|
|
946
|
+
db: PGlite,
|
|
947
|
+
mutex: Mutex,
|
|
948
|
+
changes: ChangeRecord[],
|
|
949
|
+
tableKeyColumns: Map<string, Set<string>>,
|
|
950
|
+
excludedColumns: Map<string, Set<string>>,
|
|
951
|
+
columnTypeOids: Map<string, Map<string, number>>
|
|
952
|
+
): Promise<void> {
|
|
953
|
+
const missing = new Map<string, { schema: string; table: string }>()
|
|
954
|
+
|
|
955
|
+
for (const change of changes) {
|
|
956
|
+
if (
|
|
957
|
+
tableKeyColumns.has(change.table_name) ||
|
|
958
|
+
columnTypeOids.has(change.table_name) ||
|
|
959
|
+
excludedColumns.has(change.table_name)
|
|
960
|
+
) {
|
|
961
|
+
continue
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const dot = change.table_name.indexOf('.')
|
|
965
|
+
const schema = dot === -1 ? 'public' : change.table_name.substring(0, dot)
|
|
966
|
+
const table = dot === -1 ? change.table_name : change.table_name.substring(dot + 1)
|
|
967
|
+
missing.set(`${schema}.${table}`, { schema, table })
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (missing.size === 0) return
|
|
971
|
+
|
|
972
|
+
const schemas = [...new Set([...missing.values()].map((entry) => entry.schema))]
|
|
973
|
+
const tables = [...new Set([...missing.values()].map((entry) => entry.table))]
|
|
974
|
+
|
|
975
|
+
await mutex.acquire()
|
|
976
|
+
try {
|
|
977
|
+
const schemaResult = await db.query<{
|
|
978
|
+
kind: string
|
|
979
|
+
table_schema: string
|
|
980
|
+
table_name: string
|
|
981
|
+
column_name: string
|
|
982
|
+
data_type: string | null
|
|
983
|
+
ordinal_position: number
|
|
984
|
+
}>(
|
|
985
|
+
`SELECT 'pk' AS kind, tc.table_schema, tc.table_name, kcu.column_name, NULL AS data_type, kcu.ordinal_position
|
|
986
|
+
FROM information_schema.table_constraints tc
|
|
987
|
+
JOIN information_schema.key_column_usage kcu
|
|
988
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
989
|
+
AND tc.table_schema = kcu.table_schema
|
|
990
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
991
|
+
AND tc.table_schema = ANY($1)
|
|
992
|
+
AND tc.table_name = ANY($2)
|
|
993
|
+
UNION ALL
|
|
994
|
+
SELECT 'col' AS kind, table_schema, table_name, column_name, data_type, ordinal_position
|
|
995
|
+
FROM information_schema.columns
|
|
996
|
+
WHERE table_schema = ANY($1)
|
|
997
|
+
AND table_name = ANY($2)
|
|
998
|
+
ORDER BY table_schema, table_name, kind, ordinal_position`,
|
|
999
|
+
[schemas, tables]
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
for (const row of schemaResult.rows) {
|
|
1003
|
+
const key = `${row.table_schema}.${row.table_name}`
|
|
1004
|
+
if (!missing.has(key)) continue
|
|
1005
|
+
|
|
1006
|
+
if (row.kind === 'pk') {
|
|
1007
|
+
let keys = tableKeyColumns.get(key)
|
|
1008
|
+
if (!keys) {
|
|
1009
|
+
keys = new Set()
|
|
1010
|
+
tableKeyColumns.set(key, keys)
|
|
1011
|
+
}
|
|
1012
|
+
keys.add(row.column_name)
|
|
1013
|
+
} else {
|
|
1014
|
+
if (row.data_type && UNSUPPORTED_TYPES.has(row.data_type)) {
|
|
1015
|
+
let cols = excludedColumns.get(key)
|
|
1016
|
+
if (!cols) {
|
|
1017
|
+
cols = new Set()
|
|
1018
|
+
excludedColumns.set(key, cols)
|
|
1019
|
+
}
|
|
1020
|
+
cols.add(row.column_name)
|
|
1021
|
+
}
|
|
1022
|
+
if (row.data_type) {
|
|
1023
|
+
const oid = PG_DATA_TYPE_OIDS[row.data_type]
|
|
1024
|
+
if (oid !== undefined) {
|
|
1025
|
+
let cols = columnTypeOids.get(key)
|
|
1026
|
+
if (!cols) {
|
|
1027
|
+
cols = new Map()
|
|
1028
|
+
columnTypeOids.set(key, cols)
|
|
1029
|
+
}
|
|
1030
|
+
cols.set(row.column_name, oid)
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
log.debug.repl(
|
|
1037
|
+
`refreshed metadata for ${missing.size} late table(s): ${[...missing.keys()].join(',')}`
|
|
1038
|
+
)
|
|
1039
|
+
for (const key of missing.keys()) cachedColumns.delete(key)
|
|
1040
|
+
} finally {
|
|
1041
|
+
mutex.release()
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
933
1045
|
// cache column info per table to avoid per-change allocation
|
|
934
1046
|
const cachedColumns = new Map<string, ReturnType<typeof inferColumns>>()
|
|
935
1047
|
|
|
@@ -17,7 +17,11 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
|
17
17
|
|
|
18
18
|
import { getConfig } from '../config'
|
|
19
19
|
import { startPgProxy } from '../pg-proxy'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
installChangeTracking,
|
|
22
|
+
installTriggersOnShardTables,
|
|
23
|
+
resetShardSchemaCache,
|
|
24
|
+
} from './change-tracker'
|
|
21
25
|
import { signalReplicationChange, resetReplicationState } from './handler'
|
|
22
26
|
|
|
23
27
|
import type { Server, AddressInfo } from 'node:net'
|
|
@@ -959,10 +963,10 @@ describe('zero-cache pgoutput compatibility', { timeout: 30000 }, () => {
|
|
|
959
963
|
s.close()
|
|
960
964
|
})
|
|
961
965
|
|
|
962
|
-
it('shard
|
|
966
|
+
it('streams shard mutation-confirmation tables but filters replicas', async () => {
|
|
963
967
|
// zero-cache creates shard schemas (chat_0) with clients, replicas, mutations.
|
|
964
|
-
//
|
|
965
|
-
//
|
|
968
|
+
// clients advance lmid and mutations carry server results; replicas is
|
|
969
|
+
// internal shard state that zero-cache does not expect in the change stream.
|
|
966
970
|
await db.exec(`
|
|
967
971
|
CREATE SCHEMA chat_0;
|
|
968
972
|
CREATE TABLE chat_0.clients (
|
|
@@ -976,9 +980,11 @@ describe('zero-cache pgoutput compatibility', { timeout: 30000 }, () => {
|
|
|
976
980
|
version TEXT
|
|
977
981
|
);
|
|
978
982
|
CREATE TABLE chat_0.mutations (
|
|
979
|
-
|
|
980
|
-
"clientID" TEXT,
|
|
981
|
-
|
|
983
|
+
"clientGroupID" TEXT NOT NULL,
|
|
984
|
+
"clientID" TEXT NOT NULL,
|
|
985
|
+
"mutationID" BIGINT NOT NULL,
|
|
986
|
+
result JSON,
|
|
987
|
+
PRIMARY KEY ("clientGroupID", "clientID", "mutationID")
|
|
982
988
|
);
|
|
983
989
|
`)
|
|
984
990
|
await installChangeTracking(db)
|
|
@@ -995,7 +1001,7 @@ describe('zero-cache pgoutput compatibility', { timeout: 30000 }, () => {
|
|
|
995
1001
|
)
|
|
996
1002
|
await db.exec(`INSERT INTO chat_0.replicas (id, version) VALUES ('r1', 'v1')`)
|
|
997
1003
|
await db.exec(
|
|
998
|
-
`INSERT INTO chat_0.mutations (
|
|
1004
|
+
`INSERT INTO chat_0.mutations ("clientGroupID", "clientID", "mutationID", result) VALUES ('cg1', 'c1', 1, '{}')`
|
|
999
1005
|
)
|
|
1000
1006
|
await db.exec(`INSERT INTO public.foo (id) VALUES ('normal')`)
|
|
1001
1007
|
|
|
@@ -1008,12 +1014,88 @@ describe('zero-cache pgoutput compatibility', { timeout: 30000 }, () => {
|
|
|
1008
1014
|
if (m.tag === 'insert') inserts.push(m as ZcInsert)
|
|
1009
1015
|
}
|
|
1010
1016
|
|
|
1011
|
-
// should see clients + foo inserts, but NOT replicas
|
|
1017
|
+
// should see clients + mutations + foo inserts, but NOT replicas.
|
|
1012
1018
|
const streamedTables = inserts.map((i) => `${i.relation.schema}.${i.relation.name}`)
|
|
1013
1019
|
expect(streamedTables).toContain('public.foo')
|
|
1014
1020
|
expect(streamedTables).toContain('chat_0.clients')
|
|
1021
|
+
expect(streamedTables).toContain('chat_0.mutations')
|
|
1015
1022
|
expect(streamedTables).not.toContain('chat_0.replicas')
|
|
1016
|
-
|
|
1023
|
+
|
|
1024
|
+
s.close()
|
|
1025
|
+
})
|
|
1026
|
+
|
|
1027
|
+
it('refreshes metadata for shard tables missing from cached setup', async () => {
|
|
1028
|
+
// zero-cache can create shard schemas after the first replication stream
|
|
1029
|
+
// cached metadata. the next stream must not encode bigint/json shard
|
|
1030
|
+
// columns as text, or zero-cache cannot parse mutation confirmations.
|
|
1031
|
+
const warmup = await stream()
|
|
1032
|
+
warmup.close()
|
|
1033
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
1034
|
+
|
|
1035
|
+
await db.exec(`
|
|
1036
|
+
CREATE SCHEMA chat_0;
|
|
1037
|
+
CREATE TABLE chat_0.clients (
|
|
1038
|
+
"clientGroupID" TEXT NOT NULL,
|
|
1039
|
+
"clientID" TEXT NOT NULL,
|
|
1040
|
+
"lastMutationID" BIGINT,
|
|
1041
|
+
PRIMARY KEY ("clientGroupID", "clientID")
|
|
1042
|
+
);
|
|
1043
|
+
CREATE TABLE chat_0.mutations (
|
|
1044
|
+
"clientGroupID" TEXT NOT NULL,
|
|
1045
|
+
"clientID" TEXT NOT NULL,
|
|
1046
|
+
"mutationID" BIGINT NOT NULL,
|
|
1047
|
+
result JSON,
|
|
1048
|
+
PRIMARY KEY ("clientGroupID", "clientID", "mutationID")
|
|
1049
|
+
);
|
|
1050
|
+
`)
|
|
1051
|
+
await installTriggersOnShardTables(db)
|
|
1052
|
+
|
|
1053
|
+
const s = await stream()
|
|
1054
|
+
const q = s.messages
|
|
1055
|
+
|
|
1056
|
+
await db.exec(
|
|
1057
|
+
`INSERT INTO chat_0.clients ("clientGroupID", "clientID", "lastMutationID") VALUES ('cg1', 'c1', 42)`
|
|
1058
|
+
)
|
|
1059
|
+
await db.exec(
|
|
1060
|
+
`INSERT INTO chat_0.mutations ("clientGroupID", "clientID", "mutationID", result) VALUES ('cg1', 'c1', 42, '{"data":"ok"}')`
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
let clientsInsert: ZcInsert | null = null
|
|
1064
|
+
let mutationsInsert: ZcInsert | null = null
|
|
1065
|
+
const deadline = Date.now() + 5000
|
|
1066
|
+
while (Date.now() < deadline) {
|
|
1067
|
+
const m = await q.dequeue(2000).catch(() => null)
|
|
1068
|
+
if (!m) break
|
|
1069
|
+
if (m.tag === 'insert') {
|
|
1070
|
+
const ins = m as ZcInsert
|
|
1071
|
+
if (ins.relation.schema === 'chat_0' && ins.relation.name === 'clients') {
|
|
1072
|
+
clientsInsert = ins
|
|
1073
|
+
}
|
|
1074
|
+
if (ins.relation.schema === 'chat_0' && ins.relation.name === 'mutations') {
|
|
1075
|
+
mutationsInsert = ins
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (clientsInsert && mutationsInsert) break
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
expect(clientsInsert).not.toBeNull()
|
|
1082
|
+
expect(mutationsInsert).not.toBeNull()
|
|
1083
|
+
expect([...clientsInsert!.relation.keyColumns].sort()).toEqual(
|
|
1084
|
+
['clientGroupID', 'clientID'].sort()
|
|
1085
|
+
)
|
|
1086
|
+
expect(
|
|
1087
|
+
clientsInsert!.relation.columns.find((col) => col.name === 'lastMutationID')
|
|
1088
|
+
?.typeOid
|
|
1089
|
+
).toBe(20)
|
|
1090
|
+
expect([...mutationsInsert!.relation.keyColumns].sort()).toEqual(
|
|
1091
|
+
['clientGroupID', 'clientID', 'mutationID'].sort()
|
|
1092
|
+
)
|
|
1093
|
+
expect(
|
|
1094
|
+
mutationsInsert!.relation.columns.find((col) => col.name === 'mutationID')?.typeOid
|
|
1095
|
+
).toBe(20)
|
|
1096
|
+
expect(
|
|
1097
|
+
mutationsInsert!.relation.columns.find((col) => col.name === 'result')?.typeOid
|
|
1098
|
+
).toBe(114)
|
|
1017
1099
|
|
|
1018
1100
|
s.close()
|
|
1019
1101
|
})
|
|
@@ -66,6 +66,27 @@ describe('messagePortToWs', () => {
|
|
|
66
66
|
expect(spy).toHaveBeenCalledWith('hello')
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
+
it('send() invokes the ws callback after posting', () => {
|
|
70
|
+
const [port1] = createMockPorts()
|
|
71
|
+
const ws = messagePortToWs(port1 as any)
|
|
72
|
+
const callback = vi.fn()
|
|
73
|
+
|
|
74
|
+
ws.send('hello', callback)
|
|
75
|
+
|
|
76
|
+
expect(callback).toHaveBeenCalledWith()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('send() reports an error to the callback after close', () => {
|
|
80
|
+
const [port1] = createMockPorts()
|
|
81
|
+
const ws = messagePortToWs(port1 as any)
|
|
82
|
+
const callback = vi.fn()
|
|
83
|
+
|
|
84
|
+
ws.close()
|
|
85
|
+
ws.send('ignored', callback)
|
|
86
|
+
|
|
87
|
+
expect(callback).toHaveBeenCalledWith(expect.any(Error))
|
|
88
|
+
})
|
|
89
|
+
|
|
69
90
|
it('forwards port messages as ws message events', () => {
|
|
70
91
|
const [port1, port2] = createMockPorts()
|
|
71
92
|
const ws1 = messagePortToWs(port1 as any)
|
|
@@ -26,7 +26,10 @@ interface WsCompatible {
|
|
|
26
26
|
CLOSING: number
|
|
27
27
|
CLOSED: number
|
|
28
28
|
readyState: number
|
|
29
|
-
send(
|
|
29
|
+
send(
|
|
30
|
+
data: string | ArrayBuffer | ArrayBufferView,
|
|
31
|
+
callback?: (err?: Error) => void
|
|
32
|
+
): void
|
|
30
33
|
close(code?: number, reason?: string): void
|
|
31
34
|
addEventListener(type: string, handler: (event: any) => void): void
|
|
32
35
|
removeEventListener(type: string, handler: (event: any) => void): void
|
|
@@ -143,9 +146,24 @@ export function messagePortToWs(port: MessagePort): WsCompatible {
|
|
|
143
146
|
return closed ? 3 : 1 // CLOSED or OPEN
|
|
144
147
|
},
|
|
145
148
|
|
|
146
|
-
send(data: string | ArrayBuffer | ArrayBufferView) {
|
|
147
|
-
if (closed)
|
|
148
|
-
|
|
149
|
+
send(data: string | ArrayBuffer | ArrayBufferView, callback?: (err?: Error) => void) {
|
|
150
|
+
if (closed) {
|
|
151
|
+
callback?.(new Error('WebSocket is closed'))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
port.postMessage(data)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
159
|
+
if (callback) {
|
|
160
|
+
callback(err)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
throw err
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
callback?.()
|
|
149
167
|
},
|
|
150
168
|
|
|
151
169
|
close(code?: number, _reason?: string) {
|
|
@@ -179,8 +197,19 @@ export function browserWsToWs(ws: WebSocket): WsCompatible {
|
|
|
179
197
|
return ws.readyState
|
|
180
198
|
},
|
|
181
199
|
|
|
182
|
-
send(data: string | ArrayBuffer | ArrayBufferView) {
|
|
183
|
-
|
|
200
|
+
send(data: string | ArrayBuffer | ArrayBufferView, callback?: (err?: Error) => void) {
|
|
201
|
+
try {
|
|
202
|
+
ws.send(data)
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
205
|
+
if (callback) {
|
|
206
|
+
callback(err)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
throw err
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
callback?.()
|
|
184
213
|
},
|
|
185
214
|
|
|
186
215
|
close(code?: number, reason?: string) {
|