@livestore/livestore 0.0.55-dev.1 → 0.0.55-dev.2
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/.tsbuildinfo +1 -1
- package/dist/store-devtools.d.ts +18 -0
- package/dist/store-devtools.d.ts.map +1 -0
- package/dist/store-devtools.js +145 -0
- package/dist/store-devtools.js.map +1 -0
- package/dist/store.d.ts +1 -10
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +48 -193
- package/dist/store.js.map +1 -1
- package/dist/utils/data-structures.d.ts +10 -0
- package/dist/utils/data-structures.d.ts.map +1 -0
- package/dist/utils/data-structures.js +32 -0
- package/dist/utils/data-structures.js.map +1 -0
- package/package.json +5 -5
- package/src/store-devtools.ts +213 -0
- package/src/store.ts +64 -259
- package/src/utils/data-structures.ts +36 -0
package/src/store.ts
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BootDb,
|
|
3
3
|
BootStatus,
|
|
4
|
-
DebugInfo,
|
|
5
4
|
ParamsObject,
|
|
6
5
|
PreparedBindValues,
|
|
7
6
|
ResetMode,
|
|
8
7
|
StoreAdapter,
|
|
9
8
|
StoreAdapterFactory,
|
|
10
9
|
} from '@livestore/common'
|
|
11
|
-
import {
|
|
12
|
-
Devtools,
|
|
13
|
-
getExecArgsFromMutation,
|
|
14
|
-
liveStoreVersion,
|
|
15
|
-
prepareBindValues,
|
|
16
|
-
UnexpectedError,
|
|
17
|
-
} from '@livestore/common'
|
|
10
|
+
import { Devtools, getExecArgsFromMutation, prepareBindValues, UnexpectedError } from '@livestore/common'
|
|
18
11
|
import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
|
|
19
|
-
import { makeMutationEventSchemaMemo } from '@livestore/common/schema'
|
|
20
|
-
import { assertNever, makeNoopTracer, shouldNeverHappen
|
|
12
|
+
import { makeMutationEventSchemaMemo, SCHEMA_META_TABLE, SCHEMA_MUTATIONS_META_TABLE } from '@livestore/common/schema'
|
|
13
|
+
import { assertNever, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
|
|
21
14
|
import { cuid } from '@livestore/utils/cuid'
|
|
22
15
|
import {
|
|
16
|
+
BrowserChannel,
|
|
23
17
|
Effect,
|
|
18
|
+
Either,
|
|
24
19
|
Exit,
|
|
25
20
|
FiberSet,
|
|
26
21
|
Inspectable,
|
|
@@ -38,11 +33,12 @@ import * as otel from '@opentelemetry/api'
|
|
|
38
33
|
import type { GraphQLSchema } from 'graphql'
|
|
39
34
|
|
|
40
35
|
import { globalReactivityGraph } from './global-state.js'
|
|
41
|
-
import {
|
|
36
|
+
import { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
|
|
42
37
|
import type { StackInfo } from './react/utils/stack-info.js'
|
|
43
38
|
import type { DebugRefreshReasonBase, Ref } from './reactive.js'
|
|
44
|
-
import { NOT_REFRESHED_YET } from './reactive.js'
|
|
45
39
|
import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
|
|
40
|
+
import { connectStoreToDevtools } from './store-devtools.js'
|
|
41
|
+
import { ReferenceCountedSet } from './utils/data-structures.js'
|
|
46
42
|
import { downloadBlob } from './utils/dev.js'
|
|
47
43
|
import { getDurationMsFromSpan } from './utils/otel.js'
|
|
48
44
|
|
|
@@ -154,6 +150,7 @@ export class Store<
|
|
|
154
150
|
|
|
155
151
|
readonly __mutationEventSchema
|
|
156
152
|
|
|
153
|
+
// #region constructor
|
|
157
154
|
private constructor({
|
|
158
155
|
adapter,
|
|
159
156
|
schema,
|
|
@@ -201,8 +198,20 @@ export class Store<
|
|
|
201
198
|
queriesSpanContext: otelQueriesSpanContext,
|
|
202
199
|
}
|
|
203
200
|
|
|
201
|
+
// TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
|
|
202
|
+
// But for now this is a good enough approximation with little downsides
|
|
203
|
+
const isRunningInDevtools = disableDevtools === true
|
|
204
|
+
|
|
204
205
|
// Need a set here since `schema.tables` might contain duplicates and some componentStateTables
|
|
205
|
-
const allTableNames = new Set(
|
|
206
|
+
const allTableNames = new Set(
|
|
207
|
+
// NOTE we're excluding the LiveStore schema and mutations tables as they are not user-facing
|
|
208
|
+
// unless LiveStore is running in the devtools
|
|
209
|
+
isRunningInDevtools
|
|
210
|
+
? this.schema.tables.keys()
|
|
211
|
+
: Array.from(this.schema.tables.keys()).filter(
|
|
212
|
+
(_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE,
|
|
213
|
+
),
|
|
214
|
+
)
|
|
206
215
|
const existingTableRefs = new Map(
|
|
207
216
|
Array.from(this.reactivityGraph.atoms.values())
|
|
208
217
|
.filter((_): _ is Ref<any, any, any> => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
|
|
@@ -248,6 +257,8 @@ export class Store<
|
|
|
248
257
|
}).pipe(Effect.scoped, Effect.withSpan('LiveStore:store-constructor'), FiberSet.run(fiberSet), runEffectFork)
|
|
249
258
|
}
|
|
250
259
|
|
|
260
|
+
// #endregion constructor
|
|
261
|
+
|
|
251
262
|
static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
|
|
252
263
|
storeOptions: StoreOptions<TGraphQLContext, TSchema>,
|
|
253
264
|
parentSpan: otel.Span,
|
|
@@ -314,6 +325,7 @@ export class Store<
|
|
|
314
325
|
await FiberSet.clear(this.fiberSet).pipe(Effect.withSpan('Store:destroy'), runEffectPromise)
|
|
315
326
|
}
|
|
316
327
|
|
|
328
|
+
// #region mutate
|
|
317
329
|
mutate: {
|
|
318
330
|
<const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg): void
|
|
319
331
|
(
|
|
@@ -549,6 +561,7 @@ export class Store<
|
|
|
549
561
|
},
|
|
550
562
|
)
|
|
551
563
|
}
|
|
564
|
+
// #endregion mutate
|
|
552
565
|
|
|
553
566
|
/**
|
|
554
567
|
* Directly execute a SQL query on the Store.
|
|
@@ -578,222 +591,48 @@ export class Store<
|
|
|
578
591
|
})
|
|
579
592
|
|
|
580
593
|
// #region devtools
|
|
581
|
-
// TODO shutdown behaviour
|
|
582
594
|
private bootDevtools = () =>
|
|
583
595
|
Effect.gen(this, function* () {
|
|
584
|
-
const
|
|
585
|
-
message: typeof Devtools.DevtoolsWindowMessage.MessageForContentscript.Type,
|
|
586
|
-
) => {
|
|
587
|
-
window.postMessage(Schema.encodeSync(Devtools.DevtoolsWindowMessage.MessageForContentscript)(message), '*')
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.LoadIframe.make({}))
|
|
596
|
+
// const webBridgeBroadcastChannel = yield* Devtools.WebBridge.makeBroadcastChannel()
|
|
591
597
|
|
|
598
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
|
|
599
|
+
const store = this
|
|
592
600
|
const channelId = this.adapter.coordinator.devtools.channelId
|
|
593
601
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const reactivityGraphSubcriptions = new Map<RequestId, Unsub>()
|
|
614
|
-
const liveQueriesSubscriptions = new Map<RequestId, Unsub>()
|
|
615
|
-
const debugInfoHistorySubscriptions = new Map<RequestId, Unsub>()
|
|
616
|
-
|
|
617
|
-
this.adapter.coordinator.devtools
|
|
618
|
-
.connect({ port: message.port, connectionId: this.devtoolsConnectionId })
|
|
619
|
-
.pipe(
|
|
620
|
-
Effect.tapSync(({ storeMessagePort }) => {
|
|
621
|
-
// console.log('storeMessagePort', storeMessagePort)
|
|
622
|
-
storeMessagePort.addEventListener('message', (event) => {
|
|
623
|
-
const decodedMessage = Schema.decodeUnknownSync(Devtools.MessageToAppHostStore)(event.data)
|
|
624
|
-
// console.log('storeMessagePort message', decodedMessage)
|
|
625
|
-
|
|
626
|
-
if (decodedMessage.channelId !== this.adapter.coordinator.devtools.channelId) {
|
|
627
|
-
// console.log(`Unknown message`, event)
|
|
628
|
-
return
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const requestId = decodedMessage.requestId
|
|
632
|
-
const sendToDevtools = (message: Devtools.MessageFromAppHostStore) =>
|
|
633
|
-
storeMessagePort.postMessage(Schema.encodeSync(Devtools.MessageFromAppHostStore)(message))
|
|
634
|
-
|
|
635
|
-
const requestIdleCallback = window.requestIdleCallback ?? ((cb: Function) => cb())
|
|
636
|
-
|
|
637
|
-
switch (decodedMessage._tag) {
|
|
638
|
-
case 'LSD.ReactivityGraphSubscribe': {
|
|
639
|
-
const includeResults = decodedMessage.includeResults
|
|
640
|
-
|
|
641
|
-
const send = () =>
|
|
642
|
-
// In order to not add more work to the current tick, we use requestIdleCallback
|
|
643
|
-
// to send the reactivity graph updates to the devtools
|
|
644
|
-
requestIdleCallback(
|
|
645
|
-
() =>
|
|
646
|
-
sendToDevtools(
|
|
647
|
-
Devtools.ReactivityGraphRes.make({
|
|
648
|
-
reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults }),
|
|
649
|
-
requestId,
|
|
650
|
-
channelId,
|
|
651
|
-
liveStoreVersion,
|
|
652
|
-
}),
|
|
653
|
-
),
|
|
654
|
-
{ timeout: 500 },
|
|
655
|
-
)
|
|
656
|
-
|
|
657
|
-
send()
|
|
658
|
-
|
|
659
|
-
// In some cases, there can be A LOT of reactivity graph updates in a short period of time
|
|
660
|
-
// so we throttle the updates to avoid sending too much data
|
|
661
|
-
// This might need to be tweaked further and possibly be exposed to the user in some way.
|
|
662
|
-
const throttledSend = throttle(send, 20)
|
|
663
|
-
|
|
664
|
-
reactivityGraphSubcriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
|
|
665
|
-
|
|
666
|
-
break
|
|
667
|
-
}
|
|
668
|
-
case 'LSD.DebugInfoReq': {
|
|
669
|
-
sendToDevtools(
|
|
670
|
-
Devtools.DebugInfoRes.make({
|
|
671
|
-
debugInfo: this.mainDbWrapper.debugInfo,
|
|
672
|
-
requestId,
|
|
673
|
-
channelId,
|
|
674
|
-
liveStoreVersion,
|
|
675
|
-
}),
|
|
676
|
-
)
|
|
677
|
-
break
|
|
678
|
-
}
|
|
679
|
-
case 'LSD.DebugInfoHistorySubscribe': {
|
|
680
|
-
const buffer: DebugInfo[] = []
|
|
681
|
-
let hasStopped = false
|
|
682
|
-
let rafHandle: number | undefined
|
|
683
|
-
|
|
684
|
-
const tick = () => {
|
|
685
|
-
buffer.push(this.mainDbWrapper.debugInfo)
|
|
686
|
-
|
|
687
|
-
// NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
|
|
688
|
-
// will get the empty debug info
|
|
689
|
-
// TODO We need to come up with a more graceful way to do this. Probably via a single global
|
|
690
|
-
// `requestAnimationFrame` loop that is passed in somehow.
|
|
691
|
-
this.mainDbWrapper.debugInfo = makeEmptyDebugInfo()
|
|
692
|
-
|
|
693
|
-
if (buffer.length > 10) {
|
|
694
|
-
sendToDevtools(
|
|
695
|
-
Devtools.DebugInfoHistoryRes.make({
|
|
696
|
-
debugInfoHistory: buffer,
|
|
697
|
-
requestId,
|
|
698
|
-
channelId,
|
|
699
|
-
liveStoreVersion,
|
|
700
|
-
}),
|
|
701
|
-
)
|
|
702
|
-
buffer.length = 0
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (hasStopped === false) {
|
|
706
|
-
rafHandle = requestAnimationFrame(tick)
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
rafHandle = requestAnimationFrame(tick)
|
|
711
|
-
|
|
712
|
-
const unsub = () => {
|
|
713
|
-
hasStopped = true
|
|
714
|
-
if (rafHandle !== undefined) {
|
|
715
|
-
cancelAnimationFrame(rafHandle)
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
debugInfoHistorySubscriptions.set(requestId, unsub)
|
|
720
|
-
|
|
721
|
-
break
|
|
722
|
-
}
|
|
723
|
-
case 'LSD.DebugInfoHistoryUnsubscribe': {
|
|
724
|
-
debugInfoHistorySubscriptions.get(requestId)!()
|
|
725
|
-
debugInfoHistorySubscriptions.delete(requestId)
|
|
726
|
-
break
|
|
727
|
-
}
|
|
728
|
-
case 'LSD.DebugInfoResetReq': {
|
|
729
|
-
this.mainDbWrapper.debugInfo.slowQueries.clear()
|
|
730
|
-
sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, channelId, liveStoreVersion }))
|
|
731
|
-
break
|
|
732
|
-
}
|
|
733
|
-
case 'LSD.DebugInfoRerunQueryReq': {
|
|
734
|
-
const { queryStr, bindValues, queriedTables } = decodedMessage
|
|
735
|
-
this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
|
|
736
|
-
sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, channelId, liveStoreVersion }))
|
|
737
|
-
break
|
|
738
|
-
}
|
|
739
|
-
case 'LSD.ReactivityGraphUnsubscribe': {
|
|
740
|
-
reactivityGraphSubcriptions.get(requestId)!()
|
|
741
|
-
break
|
|
742
|
-
}
|
|
743
|
-
case 'LSD.LiveQueriesSubscribe': {
|
|
744
|
-
const send = () =>
|
|
745
|
-
requestIdleCallback(
|
|
746
|
-
() =>
|
|
747
|
-
sendToDevtools(
|
|
748
|
-
Devtools.LiveQueriesRes.make({
|
|
749
|
-
liveQueries: [...this.activeQueries].map((q) => ({
|
|
750
|
-
_tag: q._tag,
|
|
751
|
-
id: q.id,
|
|
752
|
-
label: q.label,
|
|
753
|
-
runs: q.runs,
|
|
754
|
-
executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
|
|
755
|
-
lastestResult:
|
|
756
|
-
q.results$.previousResult === NOT_REFRESHED_YET
|
|
757
|
-
? 'SYMBOL_NOT_REFRESHED_YET'
|
|
758
|
-
: q.results$.previousResult,
|
|
759
|
-
activeSubscriptions: Array.from(q.activeSubscriptions),
|
|
760
|
-
})),
|
|
761
|
-
requestId,
|
|
762
|
-
liveStoreVersion,
|
|
763
|
-
channelId,
|
|
764
|
-
}),
|
|
765
|
-
),
|
|
766
|
-
{ timeout: 500 },
|
|
767
|
-
)
|
|
768
|
-
|
|
769
|
-
send()
|
|
770
|
-
|
|
771
|
-
// Same as in the reactivity graph subscription case above, we need to throttle the updates
|
|
772
|
-
const throttledSend = throttle(send, 20)
|
|
773
|
-
|
|
774
|
-
liveQueriesSubscriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
|
|
775
|
-
|
|
776
|
-
break
|
|
777
|
-
}
|
|
778
|
-
case 'LSD.LiveQueriesUnsubscribe': {
|
|
779
|
-
liveQueriesSubscriptions.get(requestId)!()
|
|
780
|
-
liveQueriesSubscriptions.delete(requestId)
|
|
781
|
-
break
|
|
782
|
-
}
|
|
783
|
-
// No default
|
|
784
|
-
}
|
|
785
|
-
})
|
|
602
|
+
// Chrome extension bridge
|
|
603
|
+
{
|
|
604
|
+
const windowChannel = yield* BrowserChannel.windowChannel({
|
|
605
|
+
window,
|
|
606
|
+
listenSchema: Devtools.DevtoolsWindowMessage.MessageForStore,
|
|
607
|
+
sendSchema: Devtools.DevtoolsWindowMessage.MessageForContentscript,
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
yield* windowChannel.send(Devtools.DevtoolsWindowMessage.LoadIframe.make({}))
|
|
611
|
+
|
|
612
|
+
yield* windowChannel.listen.pipe(
|
|
613
|
+
Stream.filterMap(Either.getRight),
|
|
614
|
+
Stream.tap((message) =>
|
|
615
|
+
Effect.gen(function* () {
|
|
616
|
+
if (message._tag === 'LSD.WindowMessage.ContentscriptListening') {
|
|
617
|
+
// Send message to contentscript via window (which the contentscript iframe is listening to)
|
|
618
|
+
yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
|
|
619
|
+
return
|
|
620
|
+
}
|
|
786
621
|
|
|
787
|
-
|
|
788
|
-
}),
|
|
789
|
-
Runtime.runFork(runtime),
|
|
790
|
-
)
|
|
622
|
+
if (message.channelId !== channelId) return
|
|
791
623
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
624
|
+
if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
|
|
625
|
+
yield* connectStoreToDevtools({ port: message.port, store })
|
|
626
|
+
}
|
|
627
|
+
}),
|
|
628
|
+
),
|
|
629
|
+
Stream.runDrain,
|
|
630
|
+
Effect.tapCauseLogPretty,
|
|
631
|
+
Effect.forkScoped,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
|
|
635
|
+
}
|
|
797
636
|
})
|
|
798
637
|
// #endregion devtools
|
|
799
638
|
|
|
@@ -810,6 +649,7 @@ export class Store<
|
|
|
810
649
|
// TODO allow for graceful store reset without requiring a full page reload (which should also call .boot)
|
|
811
650
|
dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise)
|
|
812
651
|
|
|
652
|
+
// NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
|
|
813
653
|
toJSON = () => {
|
|
814
654
|
return {
|
|
815
655
|
_tag: 'Store',
|
|
@@ -1010,43 +850,7 @@ export const createStore = <
|
|
|
1010
850
|
}
|
|
1011
851
|
// #endregion createStore
|
|
1012
852
|
|
|
1013
|
-
// TODO
|
|
1014
|
-
class ReferenceCountedSet<T> {
|
|
1015
|
-
private map: Map<T, number>
|
|
1016
|
-
|
|
1017
|
-
constructor() {
|
|
1018
|
-
this.map = new Map<T, number>()
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
add = (key: T) => {
|
|
1022
|
-
const count = this.map.get(key) ?? 0
|
|
1023
|
-
this.map.set(key, count + 1)
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
remove = (key: T) => {
|
|
1027
|
-
const count = this.map.get(key) ?? 0
|
|
1028
|
-
if (count === 1) {
|
|
1029
|
-
this.map.delete(key)
|
|
1030
|
-
} else {
|
|
1031
|
-
this.map.set(key, count - 1)
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
has = (key: T) => {
|
|
1036
|
-
return this.map.has(key)
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
get size() {
|
|
1040
|
-
return this.map.size
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
*[Symbol.iterator]() {
|
|
1044
|
-
for (const key of this.map.keys()) {
|
|
1045
|
-
yield key
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
|
|
853
|
+
// TODO propagate runtime
|
|
1050
854
|
const runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
|
|
1051
855
|
effect.pipe(
|
|
1052
856
|
Effect.tapCauseLogPretty,
|
|
@@ -1056,6 +860,7 @@ const runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
|
|
|
1056
860
|
Effect.runFork,
|
|
1057
861
|
)
|
|
1058
862
|
|
|
863
|
+
// TODO propagate runtime
|
|
1059
864
|
const runEffectPromise = <A, E>(effect: Effect.Effect<A, E, never>) =>
|
|
1060
865
|
effect.pipe(
|
|
1061
866
|
Effect.tapCauseLogPretty,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// TODO consider replacing with Effect's RC data structures
|
|
2
|
+
export class ReferenceCountedSet<T> {
|
|
3
|
+
private map: Map<T, number>
|
|
4
|
+
|
|
5
|
+
constructor() {
|
|
6
|
+
this.map = new Map<T, number>()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
add = (key: T) => {
|
|
10
|
+
const count = this.map.get(key) ?? 0
|
|
11
|
+
this.map.set(key, count + 1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
remove = (key: T) => {
|
|
15
|
+
const count = this.map.get(key) ?? 0
|
|
16
|
+
if (count === 1) {
|
|
17
|
+
this.map.delete(key)
|
|
18
|
+
} else {
|
|
19
|
+
this.map.set(key, count - 1)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
has = (key: T) => {
|
|
24
|
+
return this.map.has(key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get size() {
|
|
28
|
+
return this.map.size
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
*[Symbol.iterator]() {
|
|
32
|
+
for (const key of this.map.keys()) {
|
|
33
|
+
yield key
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|