on-zero 0.6.20 → 0.6.21
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/cjs/httpPull/transport.test.cjs +98 -2
- package/dist/cjs/httpPull/transport.test.native.js +108 -2
- package/dist/cjs/httpPull/transport.test.native.js.map +1 -1
- package/dist/cjs/httpPullTransport.cjs +88 -20
- package/dist/cjs/httpPullTransport.native.js +96 -22
- package/dist/cjs/httpPullTransport.native.js.map +1 -1
- package/dist/esm/httpPull/transport.test.mjs +74 -2
- package/dist/esm/httpPull/transport.test.mjs.map +1 -1
- package/dist/esm/httpPull/transport.test.native.js +84 -2
- package/dist/esm/httpPull/transport.test.native.js.map +1 -1
- package/dist/esm/httpPullTransport.mjs +88 -20
- package/dist/esm/httpPullTransport.mjs.map +1 -1
- package/dist/esm/httpPullTransport.native.js +96 -22
- package/dist/esm/httpPullTransport.native.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +18 -0
- package/src/httpPull/transport.test.ts +93 -4
- package/src/httpPullTransport.ts +132 -25
- package/types/httpPullTransport.d.ts +12 -1
- package/types/httpPullTransport.d.ts.map +1 -1
package/src/httpPullTransport.ts
CHANGED
|
@@ -58,6 +58,8 @@ type PullResponse =
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
type TransportState = {
|
|
61
|
+
readonly pageID: string
|
|
62
|
+
readonly transportID: string
|
|
61
63
|
readonly origin: URL
|
|
62
64
|
readonly originString: string
|
|
63
65
|
readonly pullOriginString: string
|
|
@@ -78,10 +80,15 @@ type TransportState = {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
const COOKIE_WIDTH = 20
|
|
83
|
+
// @rocicorp/zero 1.6 starts this deadline before its async createSocket work.
|
|
84
|
+
// the connect URL's `ts` is captured at the same attempt boundary.
|
|
85
|
+
const ZERO_CONNECT_TIMEOUT_MS = 10_000
|
|
81
86
|
|
|
82
87
|
export type HttpPullTransport = {
|
|
83
88
|
pull(): Promise<void>
|
|
84
89
|
readonly connections: number
|
|
90
|
+
readonly pageID: string
|
|
91
|
+
readonly transportID: string
|
|
85
92
|
uninstall(): void
|
|
86
93
|
}
|
|
87
94
|
|
|
@@ -109,24 +116,74 @@ export type HttpPullTransportOptions = {
|
|
|
109
116
|
// queries ship as name+args for the SERVER (consumer worker) to resolve with
|
|
110
117
|
// auth. the production path for permission-transformed queries.
|
|
111
118
|
queryForward?: boolean
|
|
112
|
-
//
|
|
119
|
+
// receives the structured connection lifecycle. when omitted, on-zero logs
|
|
120
|
+
// each event as one JSON line so production failures retain their owners.
|
|
113
121
|
lifecycle?: (event: HttpPullLifecycleEvent) => void
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
export type HttpPullLifecycleEvent = {
|
|
117
|
-
type: 'created' | 'listener' | 'open' | 'close' | 'failure' | 'superseded'
|
|
125
|
+
type: 'created' | 'listener' | 'open' | 'close' | 'failure' | 'superseded' | 'aborted'
|
|
126
|
+
pageID: string
|
|
127
|
+
transportID: string
|
|
128
|
+
zeroInstanceID: string
|
|
129
|
+
clientGroupID: string
|
|
130
|
+
connectionAttemptID: string
|
|
131
|
+
socketID: string
|
|
118
132
|
generation: number
|
|
119
133
|
activeGeneration: number
|
|
120
134
|
clientID: string
|
|
121
135
|
wsid: string
|
|
136
|
+
timestamp: number
|
|
137
|
+
attemptStartedAt: number | undefined
|
|
138
|
+
attemptAgeMs: number | undefined
|
|
122
139
|
listener?: SocketEventType
|
|
123
140
|
code?: number
|
|
124
141
|
reason?: string
|
|
125
142
|
}
|
|
126
143
|
|
|
144
|
+
type HttpPullPageRegistry = {
|
|
145
|
+
readonly pageID: string
|
|
146
|
+
nextTransportGeneration: number
|
|
147
|
+
readonly transportsByOrigin: Map<string, HttpPullTransport>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const HTTP_PULL_PAGE_REGISTRY = Symbol.for('on-zero.http-pull.page-registry')
|
|
151
|
+
|
|
152
|
+
function isHttpPullPageRegistry(value: unknown): value is HttpPullPageRegistry {
|
|
153
|
+
return (
|
|
154
|
+
typeof value === 'object' &&
|
|
155
|
+
value !== null &&
|
|
156
|
+
'pageID' in value &&
|
|
157
|
+
typeof value.pageID === 'string' &&
|
|
158
|
+
'nextTransportGeneration' in value &&
|
|
159
|
+
typeof value.nextTransportGeneration === 'number' &&
|
|
160
|
+
'transportsByOrigin' in value &&
|
|
161
|
+
value.transportsByOrigin instanceof Map
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getHttpPullPageRegistry(): HttpPullPageRegistry {
|
|
166
|
+
const existing = Reflect.get(globalThis, HTTP_PULL_PAGE_REGISTRY)
|
|
167
|
+
if (isHttpPullPageRegistry(existing)) return existing
|
|
168
|
+
const registry: HttpPullPageRegistry = {
|
|
169
|
+
pageID: `page-${Date.now().toString(36)}`,
|
|
170
|
+
nextTransportGeneration: 0,
|
|
171
|
+
transportsByOrigin: new Map(),
|
|
172
|
+
}
|
|
173
|
+
Reflect.set(globalThis, HTTP_PULL_PAGE_REGISTRY, registry)
|
|
174
|
+
return registry
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function logHttpPullLifecycle(event: HttpPullLifecycleEvent) {
|
|
178
|
+
if (process.env.NODE_ENV === 'test') return
|
|
179
|
+
console.info(`[on-zero:http-pull] ${JSON.stringify(event)}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
127
182
|
export function installHttpPullTransport(
|
|
128
183
|
opts: HttpPullTransportOptions,
|
|
129
184
|
): HttpPullTransport {
|
|
185
|
+
const pageRegistry = getHttpPullPageRegistry()
|
|
186
|
+
const transportID = `${pageRegistry.pageID}:transport:${++pageRegistry.nextTransportGeneration}`
|
|
130
187
|
const previousWebSocket = globalThis.WebSocket as WebSocketConstructor | undefined
|
|
131
188
|
const fetchImpl = opts.fetch ?? globalThis.fetch
|
|
132
189
|
if (!fetchImpl) {
|
|
@@ -134,6 +191,8 @@ export function installHttpPullTransport(
|
|
|
134
191
|
}
|
|
135
192
|
|
|
136
193
|
const state: TransportState = {
|
|
194
|
+
pageID: pageRegistry.pageID,
|
|
195
|
+
transportID,
|
|
137
196
|
origin: new URL(opts.origin),
|
|
138
197
|
originString: trimTrailingSlash(new URL(opts.origin).toString()),
|
|
139
198
|
pullOriginString: trimTrailingSlash(
|
|
@@ -157,7 +216,7 @@ export function installHttpPullTransport(
|
|
|
157
216
|
nextSocketGeneration: 0,
|
|
158
217
|
activeSocketGenerationByClient: new Map(),
|
|
159
218
|
activeSocketByClient: new Map(),
|
|
160
|
-
lifecycle: opts.lifecycle,
|
|
219
|
+
lifecycle: opts.lifecycle ?? logHttpPullLifecycle,
|
|
161
220
|
}
|
|
162
221
|
|
|
163
222
|
const Shim = class {
|
|
@@ -179,34 +238,40 @@ export function installHttpPullTransport(
|
|
|
179
238
|
|
|
180
239
|
globalThis.WebSocket = Shim as unknown as typeof WebSocket
|
|
181
240
|
|
|
182
|
-
|
|
241
|
+
const transport: HttpPullTransport = {
|
|
183
242
|
pull: async () => {
|
|
184
243
|
await Promise.all([...state.sockets].map((socket) => socket.pull()))
|
|
185
244
|
},
|
|
186
245
|
get connections() {
|
|
187
246
|
return state.sockets.size
|
|
188
247
|
},
|
|
248
|
+
pageID: state.pageID,
|
|
249
|
+
transportID: state.transportID,
|
|
189
250
|
uninstall: () => {
|
|
190
251
|
if (globalThis.WebSocket === (Shim as unknown as typeof WebSocket)) {
|
|
191
252
|
globalThis.WebSocket = previousWebSocket as typeof WebSocket
|
|
192
253
|
}
|
|
254
|
+
const registered = pageRegistry.transportsByOrigin.get(state.originString)
|
|
255
|
+
if (registered === transport) {
|
|
256
|
+
pageRegistry.transportsByOrigin.delete(state.originString)
|
|
257
|
+
}
|
|
193
258
|
},
|
|
194
259
|
}
|
|
260
|
+
return transport
|
|
195
261
|
}
|
|
196
262
|
|
|
197
263
|
// per-origin idempotent install for app usage (ProvideZero rotates zero
|
|
198
264
|
// instances against the same server; installing per rotation would chain
|
|
199
265
|
// shims unboundedly). installed transports live for the page lifetime.
|
|
200
|
-
const transportsByOrigin = new Map<string, HttpPullTransport>()
|
|
201
|
-
|
|
202
266
|
export function ensureHttpPullTransport(
|
|
203
267
|
opts: HttpPullTransportOptions,
|
|
204
268
|
): HttpPullTransport {
|
|
269
|
+
const pageRegistry = getHttpPullPageRegistry()
|
|
205
270
|
const key = trimTrailingSlash(new URL(opts.origin).toString())
|
|
206
|
-
const existing = transportsByOrigin.get(key)
|
|
271
|
+
const existing = pageRegistry.transportsByOrigin.get(key)
|
|
207
272
|
if (existing) return existing
|
|
208
273
|
const transport = installHttpPullTransport(opts)
|
|
209
|
-
transportsByOrigin.set(key, transport)
|
|
274
|
+
pageRegistry.transportsByOrigin.set(key, transport)
|
|
210
275
|
return transport
|
|
211
276
|
}
|
|
212
277
|
|
|
@@ -249,6 +314,11 @@ class ZeroHttpSocket {
|
|
|
249
314
|
private wakeSocket: { close(): void } | undefined
|
|
250
315
|
private wakeReconnectTimer: ReturnType<typeof setTimeout> | undefined
|
|
251
316
|
private readonly generation: number
|
|
317
|
+
private readonly zeroInstanceID: string
|
|
318
|
+
private readonly connectionAttemptID: string
|
|
319
|
+
private readonly socketID: string
|
|
320
|
+
private readonly attemptStartedAt: number | undefined
|
|
321
|
+
private settled = false
|
|
252
322
|
|
|
253
323
|
constructor(
|
|
254
324
|
private readonly state: TransportState,
|
|
@@ -261,9 +331,19 @@ class ZeroHttpSocket {
|
|
|
261
331
|
this.clientGroupID = this.connectURL.searchParams.get('clientGroupID') ?? ''
|
|
262
332
|
this.wsid = this.connectURL.searchParams.get('wsid') ?? `zero-http-${Date.now()}`
|
|
263
333
|
this.generation = ++this.state.nextSocketGeneration
|
|
264
|
-
this.state.
|
|
334
|
+
this.zeroInstanceID = `${this.state.pageID}:zero:${this.clientID}`
|
|
335
|
+
this.connectionAttemptID = `${this.zeroInstanceID}:attempt:${this.wsid}`
|
|
336
|
+
this.socketID = `${this.state.transportID}:socket:${this.generation}`
|
|
337
|
+
const attemptStartedAtParam = this.connectURL.searchParams.get('ts')
|
|
338
|
+
const attemptStartedAt = Number(attemptStartedAtParam)
|
|
339
|
+
this.attemptStartedAt =
|
|
340
|
+
attemptStartedAtParam !== null && Number.isFinite(attemptStartedAt)
|
|
341
|
+
? attemptStartedAt
|
|
342
|
+
: undefined
|
|
343
|
+
const previousSocket = this.state.activeSocketByClient.get(this.clientID)
|
|
265
344
|
this.state.activeSocketGenerationByClient.set(this.clientID, this.generation)
|
|
266
345
|
this.state.activeSocketByClient.set(this.clientID, this)
|
|
346
|
+
previousSocket?.supersede()
|
|
267
347
|
this.emitLifecycle('created')
|
|
268
348
|
const baseCookie = this.connectURL.searchParams.get('baseCookie')
|
|
269
349
|
this.cookie = baseCookie ? baseCookie : null
|
|
@@ -281,9 +361,9 @@ class ZeroHttpSocket {
|
|
|
281
361
|
this.queueDesiredQueries(decoded.initConnectionMessage?.[1])
|
|
282
362
|
|
|
283
363
|
this.state.sockets.add(this)
|
|
284
|
-
// open on the next task so Zero can attach its listeners.
|
|
285
|
-
//
|
|
286
|
-
//
|
|
364
|
+
// open on the next task so Zero can attach its listeners. open() checks the
|
|
365
|
+
// attempt timestamp first, so async socket construction that outlived Zero's
|
|
366
|
+
// deadline closes without delivering an event to abandoned state.
|
|
287
367
|
this.openTimer = setTimeout(() => this.open(), 0)
|
|
288
368
|
}
|
|
289
369
|
|
|
@@ -335,17 +415,7 @@ class ZeroHttpSocket {
|
|
|
335
415
|
}
|
|
336
416
|
|
|
337
417
|
close(code = 1000, reason = '') {
|
|
338
|
-
if (this.
|
|
339
|
-
if (this.openTimer) clearTimeout(this.openTimer)
|
|
340
|
-
if (this.pullTimer) clearInterval(this.pullTimer)
|
|
341
|
-
this.closeWakeChannel()
|
|
342
|
-
this.readyState = this.CLOSED
|
|
343
|
-
this.state.sockets.delete(this)
|
|
344
|
-
if (this.state.activeSocketByClient.get(this.clientID) === this) {
|
|
345
|
-
this.state.activeSocketByClient.delete(this.clientID)
|
|
346
|
-
this.state.activeSocketGenerationByClient.delete(this.clientID)
|
|
347
|
-
}
|
|
348
|
-
this.emitLifecycle('close', { code, reason })
|
|
418
|
+
if (!this.settle('close', { code, reason })) return
|
|
349
419
|
this.emit('close', { code, reason, wasClean: code <= 1001 })
|
|
350
420
|
}
|
|
351
421
|
|
|
@@ -376,6 +446,15 @@ class ZeroHttpSocket {
|
|
|
376
446
|
|
|
377
447
|
private open() {
|
|
378
448
|
if (this.readyState !== this.CONNECTING) return
|
|
449
|
+
const attemptAgeMs = this.getAttemptAgeMs()
|
|
450
|
+
if (attemptAgeMs !== undefined && attemptAgeMs >= ZERO_CONNECT_TIMEOUT_MS) {
|
|
451
|
+
const reason = `zero-http connection attempt ${this.wsid} expired after ${Math.round(
|
|
452
|
+
attemptAgeMs,
|
|
453
|
+
)}ms before socket construction completed`
|
|
454
|
+
if (!this.settle('aborted', { code: 1000, reason })) return
|
|
455
|
+
this.emit('close', { code: 1000, reason, wasClean: true })
|
|
456
|
+
return
|
|
457
|
+
}
|
|
379
458
|
this.readyState = this.OPEN
|
|
380
459
|
this.emitLifecycle('open')
|
|
381
460
|
this.emit('open', {})
|
|
@@ -740,13 +819,32 @@ class ZeroHttpSocket {
|
|
|
740
819
|
}
|
|
741
820
|
|
|
742
821
|
private supersede() {
|
|
743
|
-
|
|
822
|
+
this.settle('superseded')
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private settle(
|
|
826
|
+
type: Extract<HttpPullLifecycleEvent['type'], 'close' | 'superseded' | 'aborted'>,
|
|
827
|
+
detail: Pick<HttpPullLifecycleEvent, 'code' | 'reason'> = {},
|
|
828
|
+
) {
|
|
829
|
+
if (this.settled) return false
|
|
830
|
+
this.settled = true
|
|
744
831
|
if (this.openTimer !== undefined) clearTimeout(this.openTimer)
|
|
745
832
|
if (this.pullTimer !== undefined) clearInterval(this.pullTimer)
|
|
746
833
|
this.closeWakeChannel()
|
|
747
834
|
this.readyState = this.CLOSED
|
|
748
835
|
this.state.sockets.delete(this)
|
|
749
|
-
this.
|
|
836
|
+
if (this.state.activeSocketByClient.get(this.clientID) === this) {
|
|
837
|
+
this.state.activeSocketByClient.delete(this.clientID)
|
|
838
|
+
this.state.activeSocketGenerationByClient.delete(this.clientID)
|
|
839
|
+
}
|
|
840
|
+
this.emitLifecycle(type, detail)
|
|
841
|
+
return true
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private getAttemptAgeMs() {
|
|
845
|
+
return this.attemptStartedAt === undefined
|
|
846
|
+
? undefined
|
|
847
|
+
: performance.now() - this.attemptStartedAt
|
|
750
848
|
}
|
|
751
849
|
|
|
752
850
|
private emitLifecycle(
|
|
@@ -755,11 +853,20 @@ class ZeroHttpSocket {
|
|
|
755
853
|
) {
|
|
756
854
|
this.state.lifecycle?.({
|
|
757
855
|
type,
|
|
856
|
+
pageID: this.state.pageID,
|
|
857
|
+
transportID: this.state.transportID,
|
|
858
|
+
zeroInstanceID: this.zeroInstanceID,
|
|
859
|
+
clientGroupID: this.clientGroupID,
|
|
860
|
+
connectionAttemptID: this.connectionAttemptID,
|
|
861
|
+
socketID: this.socketID,
|
|
758
862
|
generation: this.generation,
|
|
759
863
|
activeGeneration:
|
|
760
864
|
this.state.activeSocketGenerationByClient.get(this.clientID) ?? this.generation,
|
|
761
865
|
clientID: this.clientID,
|
|
762
866
|
wsid: this.wsid,
|
|
867
|
+
timestamp: Date.now(),
|
|
868
|
+
attemptStartedAt: this.attemptStartedAt,
|
|
869
|
+
attemptAgeMs: this.getAttemptAgeMs(),
|
|
763
870
|
...detail,
|
|
764
871
|
})
|
|
765
872
|
}
|
|
@@ -3,6 +3,8 @@ export type QueryTransform = (name: string, args: readonly unknown[]) => unknown
|
|
|
3
3
|
export type HttpPullTransport = {
|
|
4
4
|
pull(): Promise<void>;
|
|
5
5
|
readonly connections: number;
|
|
6
|
+
readonly pageID: string;
|
|
7
|
+
readonly transportID: string;
|
|
6
8
|
uninstall(): void;
|
|
7
9
|
};
|
|
8
10
|
export type HttpPullTransportOptions = {
|
|
@@ -17,11 +19,20 @@ export type HttpPullTransportOptions = {
|
|
|
17
19
|
lifecycle?: (event: HttpPullLifecycleEvent) => void;
|
|
18
20
|
};
|
|
19
21
|
export type HttpPullLifecycleEvent = {
|
|
20
|
-
type: 'created' | 'listener' | 'open' | 'close' | 'failure' | 'superseded';
|
|
22
|
+
type: 'created' | 'listener' | 'open' | 'close' | 'failure' | 'superseded' | 'aborted';
|
|
23
|
+
pageID: string;
|
|
24
|
+
transportID: string;
|
|
25
|
+
zeroInstanceID: string;
|
|
26
|
+
clientGroupID: string;
|
|
27
|
+
connectionAttemptID: string;
|
|
28
|
+
socketID: string;
|
|
21
29
|
generation: number;
|
|
22
30
|
activeGeneration: number;
|
|
23
31
|
clientID: string;
|
|
24
32
|
wsid: string;
|
|
33
|
+
timestamp: number;
|
|
34
|
+
attemptStartedAt: number | undefined;
|
|
35
|
+
attemptAgeMs: number | undefined;
|
|
25
36
|
listener?: SocketEventType;
|
|
26
37
|
code?: number;
|
|
27
38
|
reason?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpPullTransport.d.ts","sourceRoot":"","sources":["../src/httpPullTransport.ts"],"names":[],"mappings":"AAUA,KAAK,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AA+B7D,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"httpPullTransport.d.ts","sourceRoot":"","sources":["../src/httpPullTransport.ts"],"names":[],"mappings":"AAUA,KAAK,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AA+B7D,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,CAAA;AA6ChF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,SAAS,IAAI,IAAI,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,EAAE,MAAM,CAAA;IAGd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IAGpB,cAAc,CAAC,EAAE,MAAM,CAAA;IAMvB,IAAI,CAAC,EAAE,OAAO,CAAA;IAId,cAAc,CAAC,EAAE,cAAc,CAAA;IAI/B,YAAY,CAAC,EAAE,OAAO,CAAA;IAGtB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAA;CACpD,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAAA;IACtF,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAwCD,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,wBAAwB,GAC7B,iBAAiB,CA6EnB;AAKD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,wBAAwB,GAC7B,iBAAiB,CAQnB"}
|