diff --git a/README.md b/README.md index 1aff80e3..11ab1eaf 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Library highlights: * Possibility to register a custom PUB/SUB Broker and PresenceManager implementations * Option to register custom Transport, like [Centrifugo does with WebTransport](https://centrifugal.dev/docs/transports/webtransport) * Message recovery mechanism for channels to survive PUB/SUB delivery problems, short network disconnects or node restart +* Cache channels – a way to quickly deliver latest publication from channel history to the client upon subscription +* Delta compression for publications inside a channel to reduce bandwidth usage * Out-of-the-box Prometheus instrumentation * Client SDKs for main application environments all following [single behaviour spec](https://centrifugal.dev/docs/transports/client_api) (see list of SDKs below). diff --git a/_examples/chat_json/index.html b/_examples/chat_json/index.html index 76b7bfb4..d8c671a3 100644 --- a/_examples/chat_json/index.html +++ b/_examples/chat_json/index.html @@ -224,7 +224,7 @@ // subscribe on channel and bind various event listeners. Actual // subscription request will be sent after client connects to // a server. - const sub = centrifuge.newSubscription(channel); + const sub = centrifuge.newSubscription(channel, {}); sub.on("publication", handlePublication) .on("join", handleJoin) diff --git a/_examples/chat_json/main.go b/_examples/chat_json/main.go index cfcca570..c460c47a 100644 --- a/_examples/chat_json/main.go +++ b/_examples/chat_json/main.go @@ -250,11 +250,12 @@ func main() { server := &http.Server{ Handler: mux, - Addr: ":" + strconv.Itoa(*port), + Addr: "127.0.0.1:" + strconv.Itoa(*port), ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } + log.Print("Starting server, visit http://localhost:8000") go func() { if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) diff --git a/_examples/compression_playground/apppb/app.pb.go b/_examples/compression_playground/apppb/app.pb.go new file mode 100644 index 00000000..70f98cb0 --- /dev/null +++ b/_examples/compression_playground/apppb/app.pb.go @@ -0,0 +1,475 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.25.3 +// source: app.proto + +package apppb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EventType int32 + +const ( + EventType_UNKNOWN EventType = 0 // Default value, should not be used + EventType_GOAL EventType = 1 + EventType_YELLOW_CARD EventType = 2 + EventType_RED_CARD EventType = 3 + EventType_SUBSTITUTE EventType = 4 +) + +// Enum value maps for EventType. +var ( + EventType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "GOAL", + 2: "YELLOW_CARD", + 3: "RED_CARD", + 4: "SUBSTITUTE", + } + EventType_value = map[string]int32{ + "UNKNOWN": 0, + "GOAL": 1, + "YELLOW_CARD": 2, + "RED_CARD": 3, + "SUBSTITUTE": 4, + } +) + +func (x EventType) Enum() *EventType { + p := new(EventType) + *p = x + return p +} + +func (x EventType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (EventType) Descriptor() protoreflect.EnumDescriptor { + return file_app_proto_enumTypes[0].Descriptor() +} + +func (EventType) Type() protoreflect.EnumType { + return &file_app_proto_enumTypes[0] +} + +func (x EventType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use EventType.Descriptor instead. +func (EventType) EnumDescriptor() ([]byte, []int) { + return file_app_proto_rawDescGZIP(), []int{0} +} + +type Event struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type EventType `protobuf:"varint,1,opt,name=type,proto3,enum=centrifugal.centrifuge.examples.compression_playground.EventType" json:"type,omitempty"` + Minute int32 `protobuf:"varint,2,opt,name=minute,proto3" json:"minute,omitempty"` +} + +func (x *Event) Reset() { + *x = Event{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Event) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Event) ProtoMessage() {} + +func (x *Event) ProtoReflect() protoreflect.Message { + mi := &file_app_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Event.ProtoReflect.Descriptor instead. +func (*Event) Descriptor() ([]byte, []int) { + return file_app_proto_rawDescGZIP(), []int{0} +} + +func (x *Event) GetType() EventType { + if x != nil { + return x.Type + } + return EventType_UNKNOWN +} + +func (x *Event) GetMinute() int32 { + if x != nil { + return x.Minute + } + return 0 +} + +type Player struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Events []*Event `protobuf:"bytes,2,rep,name=events,proto3" json:"events,omitempty"` +} + +func (x *Player) Reset() { + *x = Player{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Player) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Player) ProtoMessage() {} + +func (x *Player) ProtoReflect() protoreflect.Message { + mi := &file_app_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Player.ProtoReflect.Descriptor instead. +func (*Player) Descriptor() ([]byte, []int) { + return file_app_proto_rawDescGZIP(), []int{1} +} + +func (x *Player) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Player) GetEvents() []*Event { + if x != nil { + return x.Events + } + return nil +} + +type Team struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Score int32 `protobuf:"varint,2,opt,name=score,proto3" json:"score,omitempty"` + Players []*Player `protobuf:"bytes,3,rep,name=players,proto3" json:"players,omitempty"` +} + +func (x *Team) Reset() { + *x = Team{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Team) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Team) ProtoMessage() {} + +func (x *Team) ProtoReflect() protoreflect.Message { + mi := &file_app_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Team.ProtoReflect.Descriptor instead. +func (*Team) Descriptor() ([]byte, []int) { + return file_app_proto_rawDescGZIP(), []int{2} +} + +func (x *Team) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Team) GetScore() int32 { + if x != nil { + return x.Score + } + return 0 +} + +func (x *Team) GetPlayers() []*Player { + if x != nil { + return x.Players + } + return nil +} + +type Match struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + HomeTeam *Team `protobuf:"bytes,2,opt,name=home_team,json=homeTeam,proto3" json:"home_team,omitempty"` + AwayTeam *Team `protobuf:"bytes,3,opt,name=away_team,json=awayTeam,proto3" json:"away_team,omitempty"` +} + +func (x *Match) Reset() { + *x = Match{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Match) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Match) ProtoMessage() {} + +func (x *Match) ProtoReflect() protoreflect.Message { + mi := &file_app_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Match.ProtoReflect.Descriptor instead. +func (*Match) Descriptor() ([]byte, []int) { + return file_app_proto_rawDescGZIP(), []int{3} +} + +func (x *Match) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Match) GetHomeTeam() *Team { + if x != nil { + return x.HomeTeam + } + return nil +} + +func (x *Match) GetAwayTeam() *Team { + if x != nil { + return x.AwayTeam + } + return nil +} + +var File_app_proto protoreflect.FileDescriptor + +var file_app_proto_rawDesc = []byte{ + 0x0a, 0x09, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x36, 0x63, 0x65, 0x6e, + 0x74, 0x72, 0x69, 0x66, 0x75, 0x67, 0x61, 0x6c, 0x2e, 0x63, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x66, + 0x75, 0x67, 0x65, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6d, + 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6c, 0x61, 0x79, 0x67, 0x72, 0x6f, + 0x75, 0x6e, 0x64, 0x22, 0x76, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x55, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x41, 0x2e, 0x63, 0x65, 0x6e, + 0x74, 0x72, 0x69, 0x66, 0x75, 0x67, 0x61, 0x6c, 0x2e, 0x63, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x66, + 0x75, 0x67, 0x65, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6d, + 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6c, 0x61, 0x79, 0x67, 0x72, 0x6f, + 0x75, 0x6e, 0x64, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x22, 0x73, 0x0a, 0x06, 0x50, + 0x6c, 0x61, 0x79, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x55, 0x0a, 0x06, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x63, 0x65, 0x6e, 0x74, + 0x72, 0x69, 0x66, 0x75, 0x67, 0x61, 0x6c, 0x2e, 0x63, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x66, 0x75, + 0x67, 0x65, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6c, 0x61, 0x79, 0x67, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x22, 0x8a, 0x01, 0x0a, 0x04, 0x54, 0x65, 0x61, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x63, + 0x6f, 0x72, 0x65, 0x12, 0x58, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x63, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x66, 0x75, 0x67, + 0x61, 0x6c, 0x2e, 0x63, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x66, 0x75, 0x67, 0x65, 0x2e, 0x65, 0x78, + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x70, 0x6c, 0x61, 0x79, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2e, 0x50, 0x6c, + 0x61, 0x79, 0x65, 0x72, 0x52, 0x07, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x22, 0xcd, 0x01, + 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x59, 0x0a, 0x09, 0x68, 0x6f, 0x6d, 0x65, 0x5f, + 0x74, 0x65, 0x61, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x63, 0x65, 0x6e, + 0x74, 0x72, 0x69, 0x66, 0x75, 0x67, 0x61, 0x6c, 0x2e, 0x63, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x66, + 0x75, 0x67, 0x65, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6d, + 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6c, 0x61, 0x79, 0x67, 0x72, 0x6f, + 0x75, 0x6e, 0x64, 0x2e, 0x54, 0x65, 0x61, 0x6d, 0x52, 0x08, 0x68, 0x6f, 0x6d, 0x65, 0x54, 0x65, + 0x61, 0x6d, 0x12, 0x59, 0x0a, 0x09, 0x61, 0x77, 0x61, 0x79, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x63, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x66, 0x75, + 0x67, 0x61, 0x6c, 0x2e, 0x63, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x66, 0x75, 0x67, 0x65, 0x2e, 0x65, + 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6c, 0x61, 0x79, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2e, 0x54, + 0x65, 0x61, 0x6d, 0x52, 0x08, 0x61, 0x77, 0x61, 0x79, 0x54, 0x65, 0x61, 0x6d, 0x2a, 0x51, 0x0a, + 0x09, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, + 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x4f, 0x41, 0x4c, 0x10, + 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x59, 0x45, 0x4c, 0x4c, 0x4f, 0x57, 0x5f, 0x43, 0x41, 0x52, 0x44, + 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x44, 0x5f, 0x43, 0x41, 0x52, 0x44, 0x10, 0x03, + 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x55, 0x42, 0x53, 0x54, 0x49, 0x54, 0x55, 0x54, 0x45, 0x10, 0x04, + 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x3b, 0x61, 0x70, 0x70, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_app_proto_rawDescOnce sync.Once + file_app_proto_rawDescData = file_app_proto_rawDesc +) + +func file_app_proto_rawDescGZIP() []byte { + file_app_proto_rawDescOnce.Do(func() { + file_app_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_proto_rawDescData) + }) + return file_app_proto_rawDescData +} + +var file_app_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_app_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_app_proto_goTypes = []interface{}{ + (EventType)(0), // 0: centrifugal.centrifuge.examples.compression_playground.EventType + (*Event)(nil), // 1: centrifugal.centrifuge.examples.compression_playground.Event + (*Player)(nil), // 2: centrifugal.centrifuge.examples.compression_playground.Player + (*Team)(nil), // 3: centrifugal.centrifuge.examples.compression_playground.Team + (*Match)(nil), // 4: centrifugal.centrifuge.examples.compression_playground.Match +} +var file_app_proto_depIdxs = []int32{ + 0, // 0: centrifugal.centrifuge.examples.compression_playground.Event.type:type_name -> centrifugal.centrifuge.examples.compression_playground.EventType + 1, // 1: centrifugal.centrifuge.examples.compression_playground.Player.events:type_name -> centrifugal.centrifuge.examples.compression_playground.Event + 2, // 2: centrifugal.centrifuge.examples.compression_playground.Team.players:type_name -> centrifugal.centrifuge.examples.compression_playground.Player + 3, // 3: centrifugal.centrifuge.examples.compression_playground.Match.home_team:type_name -> centrifugal.centrifuge.examples.compression_playground.Team + 3, // 4: centrifugal.centrifuge.examples.compression_playground.Match.away_team:type_name -> centrifugal.centrifuge.examples.compression_playground.Team + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_app_proto_init() } +func file_app_proto_init() { + if File_app_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_app_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Event); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Player); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Team); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Match); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_app_proto_rawDesc, + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_proto_goTypes, + DependencyIndexes: file_app_proto_depIdxs, + EnumInfos: file_app_proto_enumTypes, + MessageInfos: file_app_proto_msgTypes, + }.Build() + File_app_proto = out.File + file_app_proto_rawDesc = nil + file_app_proto_goTypes = nil + file_app_proto_depIdxs = nil +} diff --git a/_examples/compression_playground/apppb/app.proto b/_examples/compression_playground/apppb/app.proto new file mode 100644 index 00000000..44a697e6 --- /dev/null +++ b/_examples/compression_playground/apppb/app.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package centrifugal.centrifuge.examples.compression_playground; + +option go_package = "./;apppb"; + +enum EventType { + UNKNOWN = 0; // Default value, should not be used + GOAL = 1; + YELLOW_CARD = 2; + RED_CARD = 3; + SUBSTITUTE = 4; +} + +message Event { + EventType type = 1; + int32 minute = 2; +} + +message Player { + string name = 1; + repeated Event events = 2; +} + +message Team { + string name = 1; + int32 score = 2; + repeated Player players = 3; +} + +message Match { + int32 id = 1; + Team home_team = 2; + Team away_team = 3; +} diff --git a/_examples/compression_playground/apppb/generate.sh b/_examples/compression_playground/apppb/generate.sh new file mode 100755 index 00000000..5d641f55 --- /dev/null +++ b/_examples/compression_playground/apppb/generate.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +# go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + +set -e + +DST_DIR=./ + +protoc -I ./ \ + app.proto \ + --go_out=${DST_DIR} \ + --go-grpc_out=${DST_DIR} diff --git a/_examples/compression_playground/main.go b/_examples/compression_playground/main.go new file mode 100644 index 00000000..f66e31bd --- /dev/null +++ b/_examples/compression_playground/main.go @@ -0,0 +1,301 @@ +package main + +import ( + "context" + "html/template" + "log" + "math/rand" + "net/http" + "strings" + "time" + + "github.com/centrifugal/centrifuge/_examples/compression_playground/apppb" + + "github.com/centrifugal/centrifuge" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +func simulateMatch(ctx context.Context, num int32, node *centrifuge.Node, useProtobufPayload bool) { + // Predefined lists of player names for each team. + playerNamesTeamA := []string{"John Doe", "Jane Smith", "Alex Johnson", "Chris Lee", "Pat Kim", "Sam Morgan", "Jamie Brown", "Casey Davis", "Morgan Garcia", "Taylor White", "Jordan Martinez"} + playerNamesTeamB := []string{"Robin Wilson", "Drew Taylor", "Jessie Bailey", "Casey Flores", "Jordan Walker", "Charlie Green", "Alex Adams", "Morgan Thompson", "Taylor Clark", "Jordan Hernandez", "Jamie Lewis"} + + // Example setup + match := &apppb.Match{ + Id: num, + HomeTeam: &apppb.Team{ + Name: "Real Madrid", + Players: assignNamesToPlayers(playerNamesTeamA), + }, + AwayTeam: &apppb.Team{ + Name: "Barcelona", + Players: assignNamesToPlayers(playerNamesTeamB), + }, + } + + totalSimulationTime := 1 // Total time for the simulation in seconds + totalEvents := 30 // Total number of events to simulate + eventInterval := float64(totalSimulationTime) / float64(totalEvents) // Time between events + + r := rand.New(rand.NewSource(37)) + + for i := 0; i < totalEvents; i++ { + // Sleep between events + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(eventInterval*1000) * time.Millisecond): + } + + // Calculate minute based on event occurrence. + minute := int(float64(i) * eventInterval / float64(totalSimulationTime) * 90) + eventType := chooseRandomEventType(r) + team := chooseRandomTeam(r, match) + playerIndex := r.Intn(11) // Choose one of the 11 players randomly + + event := &apppb.Event{Type: eventType, Minute: int32(minute)} + team.Players[playerIndex].Events = append(team.Players[playerIndex].Events, event) + + if eventType == apppb.EventType_GOAL { + team.Score++ + } + + var data []byte + var err error + + if useProtobufPayload { + data, err = proto.Marshal(match) + } else { + data, err = protojson.MarshalOptions{ + UseProtoNames: false, + }.Marshal(match) + } + if err != nil { + log.Fatal(err) + } + ch := "match:js:1" + if useProtobufPayload { + ch = "match:pb:1" + } + _, err = node.Publish( + ch, data, + centrifuge.WithDelta(true), + centrifuge.WithHistory(10, time.Minute), + ) + if err != nil { + log.Fatal(err) + } + } +} + +func chooseRandomEventType(r *rand.Rand) apppb.EventType { + events := []apppb.EventType{ + apppb.EventType_GOAL, apppb.EventType_YELLOW_CARD, apppb.EventType_RED_CARD, apppb.EventType_SUBSTITUTE} + return events[r.Intn(len(events))] +} + +func chooseRandomTeam(r *rand.Rand, match *apppb.Match) *apppb.Team { + if r.Intn(2) == 0 { + return match.HomeTeam + } + return match.AwayTeam +} + +// Helper function to create players with names from a given list +func assignNamesToPlayers(names []string) []*apppb.Player { + var players [11]*apppb.Player + for i, name := range names { + players[i] = &apppb.Player{Name: name} + } + return players[:] +} + +func auth(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + // Put authentication Credentials into request Context. + // Since we don't have any session backend here we simply + // set user ID as empty string. Users with empty ID called + // anonymous users, in real app you should decide whether + // anonymous users allowed to connect to your server or not. + cred := ¢rifuge.Credentials{ + UserID: "", + Info: []byte(r.URL.RawQuery), // This is a hack for the playground. + } + newCtx := centrifuge.SetCredentials(ctx, cred) + r = r.WithContext(newCtx) + h.ServeHTTP(w, r) + }) +} + +func main() { + // Node is the core object in Centrifuge library responsible for + // many useful things. For example Node allows publishing messages + // into channels with its Publish method. Here we initialize Node + // with Config which has reasonable defaults for zero values. + node, err := centrifuge.New(centrifuge.Config{ + LogLevel: centrifuge.LogLevelDebug, + LogHandler: func(entry centrifuge.LogEntry) { + log.Println(entry.Message, entry.Fields) + }, + //GetChannelMediumOptions: func(channel string) (centrifuge.ChannelMediumOptions, bool) { + // return centrifuge.ChannelMediumOptions{ + // KeepLatestPublication: true, + // EnableQueue: true, + // BroadcastDelay: 500 * time.Millisecond, + // }, true + //}, + }) + if err != nil { + log.Fatal(err) + } + + //redisShardConfigs := []centrifuge.RedisShardConfig{ + // {Address: "localhost:6379"}, + //} + //var redisShards []*centrifuge.RedisShard + //for _, redisConf := range redisShardConfigs { + // redisShard, err := centrifuge.NewRedisShard(node, redisConf) + // if err != nil { + // log.Fatal(err) + // } + // redisShards = append(redisShards, redisShard) + //} + // + //broker, err := centrifuge.NewRedisBroker(node, centrifuge.RedisBrokerConfig{ + // // And configure a couple of shards to use. + // Shards: redisShards, + //}) + //if err != nil { + // log.Fatal(err) + //} + //node.SetBroker(broker) + + node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) { + cred, _ := centrifuge.GetCredentials(ctx) + reply := centrifuge.ConnectReply{} + if strings.Contains(string(cred.Info), "delay") { + reply.MaxMessagesInFrame = -1 + reply.WriteDelay = 200 * time.Millisecond + reply.ReplyWithoutQueue = true + } + return reply, nil + }) + + // Set ConnectHandler called when client successfully connected to Node. + // Your code inside a handler must be synchronized since it will be called + // concurrently from different goroutines (belonging to different client + // connections). See information about connection life cycle in library readme. + // This handler should not block – so do minimal work here, set required + // connection event handlers and return. + node.OnConnect(func(client *centrifuge.Client) { + // In our example transport will always be Websocket but it can be different. + transportName := client.Transport().Name() + // In our example clients connect with JSON protocol but it can also be Protobuf. + transportProto := client.Transport().Protocol() + log.Printf("client connected via %s (%s)", transportName, transportProto) + + var useProtobufPayload bool + if strings.Contains(string(client.Info()), "protobuf") { + useProtobufPayload = true + } + + go func() { + log.Printf("using protobuf payload: %v", useProtobufPayload) + simulateMatch(client.Context(), 0, node, useProtobufPayload) + }() + + // Set SubscribeHandler to react on every channel subscription attempt + // initiated by a client. Here you can theoretically return an error or + // disconnect a client from a server if needed. But here we just accept + // all subscriptions to all channels. In real life you may use a more + // complex permission check here. The reason why we use callback style + // inside client event handlers is that it gives a possibility to control + // operation concurrency to developer and still control order of events. + client.OnSubscribe(func(e centrifuge.SubscribeEvent, cb centrifuge.SubscribeCallback) { + log.Printf("client subscribes on channel %s", e.Channel) + cb(centrifuge.SubscribeReply{ + Options: centrifuge.SubscribeOptions{ + EnableRecovery: true, + RecoveryMode: centrifuge.RecoveryModeCache, + AllowedDeltaTypes: []centrifuge.DeltaType{centrifuge.DeltaTypeFossil}, + }, + }, nil) + }) + + // By default, clients can not publish messages into channels. By setting + // PublishHandler we tell Centrifuge that publish from a client-side is + // possible. Now each time client calls publish method this handler will be + // called and you have a possibility to validate publication request. After + // returning from this handler Publication will be published to a channel and + // reach active subscribers with at most once delivery guarantee. In our simple + // chat app we allow everyone to publish into any channel but in real case + // you may have more validation. + client.OnPublish(func(e centrifuge.PublishEvent, cb centrifuge.PublishCallback) { + log.Printf("client publishes into channel %s: %s", e.Channel, string(e.Data)) + cb(centrifuge.PublishReply{}, nil) + }) + + // Set Disconnect handler to react on client disconnect events. + client.OnDisconnect(func(e centrifuge.DisconnectEvent) { + log.Printf("client disconnected: %d (%s)", e.Code, e.Reason) + }) + }) + + // Run node. This method does not block. See also node.Shutdown method + // to finish application gracefully. + if err := node.Run(); err != nil { + log.Fatal(err) + } + + // Now configure HTTP routes. + + http.Handle("/connection/websocket/no_compression", auth(centrifuge.NewWebsocketHandler(node, centrifuge.WebsocketConfig{}))) + + http.Handle("/connection/websocket/with_compression", auth(centrifuge.NewWebsocketHandler(node, centrifuge.WebsocketConfig{ + Compression: true, + CompressionMinSize: 1, + CompressionLevel: 1, + }))) + + http.HandleFunc("/", serveIndex) + http.HandleFunc("/json", serveJsonApp) + http.HandleFunc("/protobuf", serveProtobufApp) + + // Serve static files from the /static folder + fs := http.FileServer(http.Dir("static")) + http.Handle("/static/", http.StripPrefix("/static/", fs)) + + log.Printf("Starting server, visit http://localhost:8000") + if err := http.ListenAndServe("127.0.0.1:8000", nil); err != nil { + log.Fatal(err) + } +} + +func serveIndex(w http.ResponseWriter, r *http.Request) { + t, err := template.ParseFiles("templates/index.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _ = t.Execute(w, nil) +} + +func serveJsonApp(w http.ResponseWriter, r *http.Request) { + t, err := template.ParseFiles("templates/json.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _ = t.Execute(w, nil) +} + +func serveProtobufApp(w http.ResponseWriter, r *http.Request) { + t, err := template.ParseFiles("templates/protobuf.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _ = t.Execute(w, nil) +} diff --git a/_examples/compression_playground/readme.md b/_examples/compression_playground/readme.md new file mode 100644 index 00000000..c3d46e30 --- /dev/null +++ b/_examples/compression_playground/readme.md @@ -0,0 +1,37 @@ +This is a sample simulation of football match where the entire state is sent into WebSocket connection upon every +match event. The example is not very idiomatic because we try to simulate various modes thus several different +files were required. In practice, you will have JSON or Protobuf case only, and there is no need to tweak behaviour +over URL params like we do here. + +The goal was to compare different compression strategies for WebSocket data transfer. Please note, that results +depend a lot on the data you send. You may get absolutely different results for your data. Still we hope this +example gives some insights on how to choose the best compression strategy and what to expect from Centrifuge. + +Results with different configurations for total data sent over the interface from server to client, +caught with WireShark filter: + +``` +tcp.srcport == 8000 && websocket +``` + +| Protocol | Compression | Delta | Bytes sent | Percentage | +|------------------------------|-------------|-----------|------------|------------| +| JSON over JSON | No | No | 40251 | 100.0 | +| JSON over JSON | Yes | No | 15669 | 38.93 | +| JSON over JSON | No | Yes | 6043 | 15.01 | +| JSON over JSON | Yes | Yes | 5360 | 13.32 | +| JSON over Protobuf | No | No | 39180 | 97.34 | +| JSON over Protobuf | Yes | No | 15542 | 38.61 | +| JSON over Protobuf | No | Yes | 4287 | 10.65 | +| JSON over Protobuf | Yes | Yes | 4126 | 10.25 | +| Protobuf over Protobuf | No | No | 16562 | 41.15 | +| Protobuf over Protobuf | Yes | No | 13115 | 32.58 | +| Protobuf over Protobuf | No | Yes | 4382 | 10.89 | +| Protobuf over Protobuf | Yes | Yes | 4473 | 11.11 | +| JSON over JSON 200ms | Yes | Yes | 2060 | 5.12 | +| JSON over Protobuf 200ms | Yes | Yes | 2008 | 4.99 | +| Protobuf over Protobuf 200ms | Yes | Yes | 2315 | 5.75 | + +Note: since we send JSON over Protobuf, the JSON size is the same as the JSON over JSON case. +In this case Centrifugal protocol gives lower overhead, but the main part comes from the JSON payload size. +Another advantage of JSON over Protobuf is that we are not forced to use base64 encoding for delta case. diff --git a/_examples/compression_playground/static/app.css b/_examples/compression_playground/static/app.css new file mode 100644 index 00000000..d3a83b95 --- /dev/null +++ b/_examples/compression_playground/static/app.css @@ -0,0 +1,48 @@ +html, body { + height: 100%; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + font-family: 'Arial', sans-serif; + background-color: #f0f0f0; + color: #333; +} + +#app { + width: 100%; + max-width: 800px; /* Larger width for better display of big elements */ + padding: 40px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); + background-color: white; + border-radius: 10px; + margin: 20px; +} + +#app li { + margin-bottom: 5px; +} + +#app h2 { + font-size: 36px; /* Larger heading */ + color: #444; + margin-bottom: 30px; +} + +#app p { + font-size: 28px; /* Larger font size for the scores */ + line-height: 1.6; + margin: 20px 0; + color: #555; +} + +/* Styling for team names to make them stand out */ +#app p span.team-name { + font-weight: bold; +} + +/* Making the scores pop out even more */ +#app p span.team-score { + color: #d9534f; /* A vibrant color for the scores */ + font-weight: bold; +} \ No newline at end of file diff --git a/_examples/compression_playground/static/app.proto b/_examples/compression_playground/static/app.proto new file mode 100644 index 00000000..44a697e6 --- /dev/null +++ b/_examples/compression_playground/static/app.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package centrifugal.centrifuge.examples.compression_playground; + +option go_package = "./;apppb"; + +enum EventType { + UNKNOWN = 0; // Default value, should not be used + GOAL = 1; + YELLOW_CARD = 2; + RED_CARD = 3; + SUBSTITUTE = 4; +} + +message Event { + EventType type = 1; + int32 minute = 2; +} + +message Player { + string name = 1; + repeated Event events = 2; +} + +message Team { + string name = 1; + int32 score = 2; + repeated Player players = 3; +} + +message Match { + int32 id = 1; + Team home_team = 2; + Team away_team = 3; +} diff --git a/_examples/compression_playground/templates/index.html b/_examples/compression_playground/templates/index.html new file mode 100644 index 00000000..18f1366d --- /dev/null +++ b/_examples/compression_playground/templates/index.html @@ -0,0 +1,59 @@ + + + + + + + + +
+
    +
  1. + JSON over JSON, no compression, no delta +
  2. +
  3. + JSON over JSON, with compression, no delta +
  4. +
  5. + JSON over JSON, no compression, with delta +
  6. +
  7. + JSON over JSON, with compression, with delta +
  8. +
  9. + JSON over Protobuf, no compression, no delta +
  10. +
  11. + JSON over Protobuf, with compression, no delta +
  12. +
  13. + JSON over Protobuf, no compression, with delta +
  14. +
  15. + JSON over Protobuf, with compression, with delta +
  16. +
  17. + Protobuf over Protobuf, no compression, no delta +
  18. +
  19. + Protobuf over Protobuf, with compression, no delta +
  20. +
  21. + Protobuf over Protobuf, no compression, with delta +
  22. +
  23. + Protobuf over Protobuf, with compression, with delta +
  24. +
  25. + JSON over JSON, with compression, with delta, with delay +
  26. +
  27. + JSON over Protobuf, with compression, with delta, with delay +
  28. +
  29. + Protobuf over Protobuf, with compression, with delta, with delay +
  30. +
+
+ + diff --git a/_examples/compression_playground/templates/json.html b/_examples/compression_playground/templates/json.html new file mode 100644 index 00000000..68e72c3b --- /dev/null +++ b/_examples/compression_playground/templates/json.html @@ -0,0 +1,103 @@ + + + + + + + + + + + + Home +
+ + diff --git a/_examples/compression_playground/templates/protobuf.html b/_examples/compression_playground/templates/protobuf.html new file mode 100644 index 00000000..f298450c --- /dev/null +++ b/_examples/compression_playground/templates/protobuf.html @@ -0,0 +1,103 @@ + + + + + + + + + + + + Home +
+ + diff --git a/_examples/custom_broker_nats/natsbroker/broker.go b/_examples/custom_broker_nats/natsbroker/broker.go index 14eac0d1..7a5a65d2 100644 --- a/_examples/custom_broker_nats/natsbroker/broker.go +++ b/_examples/custom_broker_nats/natsbroker/broker.go @@ -192,7 +192,7 @@ func (b *NatsBroker) handleClientMessage(subject string, data []byte) error { if err != nil { return err } - _ = b.eventHandler.HandlePublication(channel, &pub, centrifuge.StreamPosition{}) + _ = b.eventHandler.HandlePublication(channel, &pub, centrifuge.StreamPosition{}, nil) case joinPushType: var info centrifuge.ClientInfo err := json.Unmarshal(p.Data, &info) diff --git a/_examples/custom_engine_tarantool/tntengine/broker.go b/_examples/custom_engine_tarantool/tntengine/broker.go index 791710ea..097532a1 100644 --- a/_examples/custom_engine_tarantool/tntengine/broker.go +++ b/_examples/custom_engine_tarantool/tntengine/broker.go @@ -720,7 +720,8 @@ func (b *Broker) handleMessage(eventHandler centrifuge.BrokerEventHandler, msg p pub.Info = infoFromProto(&info) } } - _ = eventHandler.HandlePublication(msg.Channel, pub, centrifuge.StreamPosition{Offset: msg.Offset, Epoch: msg.Epoch}) + _ = eventHandler.HandlePublication( + msg.Channel, pub, centrifuge.StreamPosition{Offset: msg.Offset, Epoch: msg.Epoch}, nil) case "j": var info protocol.ClientInfo err := info.UnmarshalVT(msg.Info) @@ -796,7 +797,12 @@ func (b *Broker) runControlPubSub(s *Shard, eventHandler centrifuge.BrokerEventH case n := <-workCh: err := eventHandler.HandleControl(n.Data) if err != nil { - b.node.Log(centrifuge.NewLogEntry(centrifuge.LogLevelError, "error handling control message", map[string]any{"error": err.Error()})) + b.node.Log( + centrifuge.NewLogEntry( + centrifuge.LogLevelError, "error handling control message", + map[string]any{"error": err.Error()}, + ), + ) continue } } diff --git a/_examples/emulation/main.go b/_examples/emulation/main.go index c73a6a39..c54d56ef 100644 --- a/_examples/emulation/main.go +++ b/_examples/emulation/main.go @@ -81,34 +81,34 @@ func main() { LogHandler: handleLog, }) - redisShardConfigs := []centrifuge.RedisShardConfig{ - {Address: "localhost:6379"}, - } - var redisShards []*centrifuge.RedisShard - for _, redisConf := range redisShardConfigs { - redisShard, err := centrifuge.NewRedisShard(node, redisConf) - if err != nil { - log.Fatal(err) - } - redisShards = append(redisShards, redisShard) - } - - broker, err := centrifuge.NewRedisBroker(node, centrifuge.RedisBrokerConfig{ - // And configure a couple of shards to use. - Shards: redisShards, - }) - if err != nil { - log.Fatal(err) - } - node.SetBroker(broker) - - presenceManager, err := centrifuge.NewRedisPresenceManager(node, centrifuge.RedisPresenceManagerConfig{ - Shards: redisShards, - }) - if err != nil { - log.Fatal(err) - } - node.SetPresenceManager(presenceManager) + //redisShardConfigs := []centrifuge.RedisShardConfig{ + // {Address: "localhost:6379"}, + //} + //var redisShards []*centrifuge.RedisShard + //for _, redisConf := range redisShardConfigs { + // redisShard, err := centrifuge.NewRedisShard(node, redisConf) + // if err != nil { + // log.Fatal(err) + // } + // redisShards = append(redisShards, redisShard) + //} + // + //broker, err := centrifuge.NewRedisBroker(node, centrifuge.RedisBrokerConfig{ + // // And configure a couple of shards to use. + // Shards: redisShards, + //}) + //if err != nil { + // log.Fatal(err) + //} + //node.SetBroker(broker) + // + //presenceManager, err := centrifuge.NewRedisPresenceManager(node, centrifuge.RedisPresenceManagerConfig{ + // Shards: redisShards, + //}) + //if err != nil { + // log.Fatal(err) + //} + //node.SetPresenceManager(presenceManager) node.OnConnecting(func(ctx context.Context, e centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) { cred, _ := centrifuge.GetCredentials(ctx) diff --git a/_examples/go.mod b/_examples/go.mod index b11331d2..702d151f 100644 --- a/_examples/go.mod +++ b/_examples/go.mod @@ -7,7 +7,7 @@ replace github.com/centrifugal/centrifuge => ../ require ( github.com/FZambia/tarantool v0.2.2 github.com/centrifugal/centrifuge v0.8.2 - github.com/centrifugal/protocol v0.12.1 + github.com/centrifugal/protocol v0.13.0 github.com/cristalhq/jwt/v5 v5.4.0 github.com/dchest/uniuri v1.2.0 github.com/gin-contrib/sessions v0.0.3 @@ -70,9 +70,10 @@ require ( github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/redis/rueidis v1.0.37-0.20240510165047-ebd66b7de128 // indirect + github.com/redis/rueidis v1.0.37 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/encoding v0.4.0 // indirect + github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/_examples/go.sum b/_examples/go.sum index 14471582..0a724002 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -14,8 +14,8 @@ github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/centrifugal/protocol v0.12.1 h1:hGbIl9Y0UbVsESgLcsqgZ7duwEnrZebFUYdu5Opwzgo= -github.com/centrifugal/protocol v0.12.1/go.mod h1:5Z0SuNdXEt83Fkoi34BCyY23p1P8+zQakQS6/BfJHak= +github.com/centrifugal/protocol v0.13.0 h1:3j9CWlbML5O9OlhLmSPWgptby0hDn4pQC9W+q6UiQQo= +github.com/centrifugal/protocol v0.13.0/go.mod h1:lM54PGU/u5WupYSb755Zv6tZ2ju1SqNKCp6A4s0DeG4= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= @@ -144,14 +144,16 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM= github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= -github.com/redis/rueidis v1.0.37-0.20240510165047-ebd66b7de128 h1:wjJwFyRE8EYJVASGhwnQ7oI2uPdRrCGuPKiI8kpgGLE= -github.com/redis/rueidis v1.0.37-0.20240510165047-ebd66b7de128/go.mod h1:bnbkk4+CkXZgDPEbUtSos/o55i4RhFYYesJ4DS2zmq0= +github.com/redis/rueidis v1.0.37 h1:RBb1s97wcvlK94YZvyh+B/c6zOkc0ssamlfWRGfRlaw= +github.com/redis/rueidis v1.0.37/go.mod h1:bnbkk4+CkXZgDPEbUtSos/o55i4RhFYYesJ4DS2zmq0= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.4.0 h1:MEBYvRqiUB2nfR2criEXWqwdY6HJOUrCn5hboVOVmy8= github.com/segmentio/encoding v0.4.0/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= +github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b h1:SCYeryKXBVdW38167VyumGakH+7E4Wxe6b/zxmQxwyM= +github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b/go.mod h1:daNLfX/GJKuZyN4HkMf0h8dVmTmgRbBSkd9bFQyGNIo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/_examples/recovery_mode_cache/centrifuge.js b/_examples/recovery_mode_cache/centrifuge.js new file mode 100644 index 00000000..ab550cbe --- /dev/null +++ b/_examples/recovery_mode_cache/centrifuge.js @@ -0,0 +1,7 @@ +"use strict";(()=>{var ne=Object.create;var N=Object.defineProperty;var ie=Object.getOwnPropertyDescriptor;var se=Object.getOwnPropertyNames;var re=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var ae=(o,s)=>()=>(s||o((s={exports:{}}).exports,s),s.exports);var ce=(o,s,e,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let n of se(s))!oe.call(o,n)&&n!==e&&N(o,n,{get:()=>s[n],enumerable:!(t=ie(s,n))||t.enumerable});return o};var W=(o,s,e)=>(e=o!=null?ne(re(o)):{},ce(s||!o||!o.__esModule?N(e,"default",{value:o,enumerable:!0}):e,o));var M=ae((Se,A)=>{"use strict";var y=typeof Reflect=="object"?Reflect:null,q=y&&typeof y.apply=="function"?y.apply:function(s,e,t){return Function.prototype.apply.call(s,e,t)},P;y&&typeof y.ownKeys=="function"?P=y.ownKeys:Object.getOwnPropertySymbols?P=function(s){return Object.getOwnPropertyNames(s).concat(Object.getOwnPropertySymbols(s))}:P=function(s){return Object.getOwnPropertyNames(s)};function ue(o){console&&console.warn&&console.warn(o)}var H=Number.isNaN||function(s){return s!==s};function u(){u.init.call(this)}A.exports=u;A.exports.once=pe;u.EventEmitter=u;u.prototype._events=void 0;u.prototype._eventsCount=0;u.prototype._maxListeners=void 0;var J=10;function C(o){if(typeof o!="function")throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof o)}Object.defineProperty(u,"defaultMaxListeners",{enumerable:!0,get:function(){return J},set:function(o){if(typeof o!="number"||o<0||H(o))throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received '+o+".");J=o}});u.init=function(){(this._events===void 0||this._events===Object.getPrototypeOf(this)._events)&&(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0};u.prototype.setMaxListeners=function(s){if(typeof s!="number"||s<0||H(s))throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received '+s+".");return this._maxListeners=s,this};function F(o){return o._maxListeners===void 0?u.defaultMaxListeners:o._maxListeners}u.prototype.getMaxListeners=function(){return F(this)};u.prototype.emit=function(s){for(var e=[],t=1;t0&&(r=e[0]),r instanceof Error)throw r;var a=new Error("Unhandled error."+(r?" ("+r.message+")":""));throw a.context=r,a}var c=i[s];if(c===void 0)return!1;if(typeof c=="function")q(c,this,e);else for(var h=c.length,m=V(c,h),t=0;t0&&r.length>n&&!r.warned){r.warned=!0;var a=new Error("Possible EventEmitter memory leak detected. "+r.length+" "+String(s)+" listeners added. Use emitter.setMaxListeners() to increase limit");a.name="MaxListenersExceededWarning",a.emitter=o,a.type=s,a.count=r.length,ue(a)}return o}u.prototype.addListener=function(s,e){return B(this,s,e,!1)};u.prototype.on=u.prototype.addListener;u.prototype.prependListener=function(s,e){return B(this,s,e,!0)};function he(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,arguments.length===0?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function K(o,s,e){var t={fired:!1,wrapFn:void 0,target:o,type:s,listener:e},n=he.bind(t);return n.listener=e,t.wrapFn=n,n}u.prototype.once=function(s,e){return C(e),this.on(s,K(this,s,e)),this};u.prototype.prependOnceListener=function(s,e){return C(e),this.prependListener(s,K(this,s,e)),this};u.prototype.removeListener=function(s,e){var t,n,i,r,a;if(C(e),n=this._events,n===void 0)return this;if(t=n[s],t===void 0)return this;if(t===e||t.listener===e)--this._eventsCount===0?this._events=Object.create(null):(delete n[s],n.removeListener&&this.emit("removeListener",s,t.listener||e));else if(typeof t!="function"){for(i=-1,r=t.length-1;r>=0;r--)if(t[r]===e||t[r].listener===e){a=t[r].listener,i=r;break}if(i<0)return this;i===0?t.shift():le(t,i),t.length===1&&(n[s]=t[0]),n.removeListener!==void 0&&this.emit("removeListener",s,a||e)}return this};u.prototype.off=u.prototype.removeListener;u.prototype.removeAllListeners=function(s){var e,t,n;if(t=this._events,t===void 0)return this;if(t.removeListener===void 0)return arguments.length===0?(this._events=Object.create(null),this._eventsCount=0):t[s]!==void 0&&(--this._eventsCount===0?this._events=Object.create(null):delete t[s]),this;if(arguments.length===0){var i=Object.keys(t),r;for(n=0;n=0;n--)this.removeListener(s,e[n]);return this};function G(o,s,e){var t=o._events;if(t===void 0)return[];var n=t[s];return n===void 0?[]:typeof n=="function"?e?[n.listener||n]:[n]:e?fe(n):V(n,n.length)}u.prototype.listeners=function(s){return G(this,s,!0)};u.prototype.rawListeners=function(s){return G(this,s,!1)};u.listenerCount=function(o,s){return typeof o.listenerCount=="function"?o.listenerCount(s):Q.call(o,s)};u.prototype.listenerCount=Q;function Q(o){var s=this._events;if(s!==void 0){var e=s[o];if(typeof e=="function")return 1;if(e!==void 0)return e.length}return 0}u.prototype.eventNames=function(){return this._eventsCount>0?P(this._events):[]};function V(o,s){for(var e=new Array(s),t=0;t(t.Disconnected="disconnected",t.Connecting="connecting",t.Connected="connected",t))(k||{}),R=(t=>(t.Unsubscribed="unsubscribed",t.Subscribing="subscribing",t.Subscribed="subscribed",t))(R||{});function Y(o,s){return o.lastIndexOf(s,0)===0}function z(o){return o==null?!1:typeof o=="function"}function Z(o,s){if(globalThis.console){let e=globalThis.console[o];z(e)&&e.apply(globalThis.console,s)}}function _e(o,s){return Math.floor(Math.random()*(s-o+1)+o)}function S(o,s,e){o>31&&(o=31);let t=_e(0,Math.min(e,s*Math.pow(2,o)));return Math.min(e,s+t)}function $(o){return"error"in o&&o.error!==null}function x(o){return Math.min(o*1e3,2147483647)}var O=class extends ee.default{constructor(e,t,n){super();this._resubscribeTimeout=null;this._refreshTimeout=null;this.channel=t,this.state="unsubscribed",this._centrifuge=e,this._token="",this._getToken=null,this._data=null,this._getData=null,this._recover=!1,this._offset=null,this._epoch=null,this._recoverable=!1,this._positioned=!1,this._joinLeave=!1,this._minResubscribeDelay=500,this._maxResubscribeDelay=2e4,this._resubscribeTimeout=null,this._resubscribeAttempts=0,this._promises={},this._promiseId=0,this._inflight=!1,this._refreshTimeout=null,this._setOptions(n),this._centrifuge._debugEnabled?(this.on("state",i=>{this._centrifuge._debug("subscription state",t,i.oldState,"->",i.newState)}),this.on("error",i=>{this._centrifuge._debug("subscription error",t,i)})):this.on("error",function(){Function.prototype()})}ready(e){return this.state==="unsubscribed"?Promise.reject({code:7,message:this.state}):this.state==="subscribed"?Promise.resolve():new Promise((t,n)=>{let i={resolve:t,reject:n};e&&(i.timeout=setTimeout(function(){n({code:1,message:"timeout"})},e)),this._promises[this._nextPromiseId()]=i})}subscribe(){this._isSubscribed()||(this._resubscribeAttempts=0,this._setSubscribing(0,"subscribe called"))}unsubscribe(){this._setUnsubscribed(0,"unsubscribe called",!0)}publish(e){let t=this;return this._methodCall().then(function(){return t._centrifuge.publish(t.channel,e)})}presence(){let e=this;return this._methodCall().then(function(){return e._centrifuge.presence(e.channel)})}presenceStats(){let e=this;return this._methodCall().then(function(){return e._centrifuge.presenceStats(e.channel)})}history(e){let t=this;return this._methodCall().then(function(){return t._centrifuge.history(t.channel,e)})}_methodCall(){return this._isSubscribed()?Promise.resolve():this._isUnsubscribed()?Promise.reject({code:7,message:this.state}):new Promise((e,t)=>{let n=setTimeout(function(){t({code:1,message:"timeout"})},this._centrifuge._config.timeout);this._promises[this._nextPromiseId()]={timeout:n,resolve:e,reject:t}})}_nextPromiseId(){return++this._promiseId}_needRecover(){return this._recover===!0}_isUnsubscribed(){return this.state==="unsubscribed"}_isSubscribing(){return this.state==="subscribing"}_isSubscribed(){return this.state==="subscribed"}_setState(e){if(this.state!==e){let t=this.state;return this.state=e,this.emit("state",{newState:e,oldState:t,channel:this.channel}),!0}return!1}_usesToken(){return this._token!==""||this._getToken!==null}_clearSubscribingState(){this._resubscribeAttempts=0,this._clearResubscribeTimeout()}_clearSubscribedState(){this._clearRefreshTimeout()}_setSubscribed(e){if(!this._isSubscribing())return;this._clearSubscribingState(),e.recoverable&&(this._recover=!0,this._offset=e.offset||0,this._epoch=e.epoch||""),this._setState("subscribed");let t=this._centrifuge._getSubscribeContext(this.channel,e);this.emit("subscribed",t),this._resolvePromises();let n=e.publications;if(n&&n.length>0)for(let i in n)n.hasOwnProperty(i)&&this._handlePublication(n[i]);e.expires===!0&&(this._refreshTimeout=setTimeout(()=>this._refresh(),x(e.ttl)))}_setSubscribing(e,t){this._isSubscribing()||(this._isSubscribed()&&this._clearSubscribedState(),this._setState("subscribing")&&this.emit("subscribing",{channel:this.channel,code:e,reason:t}),this._subscribe())}_subscribe(){if(this._centrifuge._debug("subscribing on",this.channel),!this._centrifuge._transportIsOpen)return this._centrifuge._debug("delay subscribe on",this.channel,"till connected"),null;let e=this,t={channel:e.channel};return!this._usesToken()||this._token?e._getData?(e._getData(t).then(function(n){e._isSubscribing()&&(e._data=n,e._sendSubscribe(e._token))}),null):e._sendSubscribe(e._token):(this._getSubscriptionToken().then(function(n){if(e._isSubscribing()){if(!n){e._failUnauthorized();return}e._token=n,e._getData?e._getData(t).then(function(i){e._isSubscribing()&&(e._data=i,e._sendSubscribe(n))}):e._sendSubscribe(n)}}).catch(function(n){if(e._isSubscribing()){if(n instanceof b){e._failUnauthorized();return}e.emit("error",{type:"subscribeToken",channel:e.channel,error:{code:8,message:n!==void 0?n.toString():""}}),e._scheduleResubscribe()}}),null)}_sendSubscribe(e){if(!this._centrifuge._transportIsOpen)return null;let n={channel:this.channel};if(e&&(n.token=e),this._data&&(n.data=this._data),this._positioned&&(n.positioned=!0),this._recoverable&&(n.recoverable=!0),this._joinLeave&&(n.join_leave=!0),this._needRecover()){n.recover=!0;let r=this._getOffset();r&&(n.offset=r);let a=this._getEpoch();a&&(n.epoch=a)}let i={subscribe:n};return this._inflight=!0,this._centrifuge._call(i).then(r=>{this._inflight=!1;let a=r.reply.subscribe;this._handleSubscribeResponse(a),r.next&&r.next()},r=>{this._inflight=!1,this._handleSubscribeError(r.error),r.next&&r.next()}),i}_handleSubscribeError(e){if(this._isSubscribing()){if(e.code===1){this._centrifuge._disconnect(3,"subscribe timeout",!0);return}this._subscribeError(e)}}_handleSubscribeResponse(e){this._isSubscribing()&&this._setSubscribed(e)}_setUnsubscribed(e,t,n){this._isUnsubscribed()||(this._isSubscribed()&&(n&&this._centrifuge._unsubscribe(this),this._clearSubscribedState()),this._isSubscribing()&&(this._inflight&&n&&this._centrifuge._unsubscribe(this),this._clearSubscribingState()),this._setState("unsubscribed")&&this.emit("unsubscribed",{channel:this.channel,code:e,reason:t}),this._rejectPromises({code:7,message:this.state}))}_handlePublication(e){let t=this._centrifuge._getPublicationContext(this.channel,e);this.emit("publication",t),e.offset&&(this._offset=e.offset)}_handleJoin(e){let t=this._centrifuge._getJoinLeaveContext(e.info);this.emit("join",{channel:this.channel,info:t})}_handleLeave(e){let t=this._centrifuge._getJoinLeaveContext(e.info);this.emit("leave",{channel:this.channel,info:t})}_resolvePromises(){for(let e in this._promises)this._promises.hasOwnProperty(e)&&(this._promises[e].timeout&&clearTimeout(this._promises[e].timeout),this._promises[e].resolve(),delete this._promises[e])}_rejectPromises(e){for(let t in this._promises)this._promises.hasOwnProperty(t)&&(this._promises[t].timeout&&clearTimeout(this._promises[t].timeout),this._promises[t].reject(e),delete this._promises[t])}_scheduleResubscribe(){let e=this,t=this._getResubscribeDelay();this._resubscribeTimeout=setTimeout(function(){e._isSubscribing()&&e._subscribe()},t)}_subscribeError(e){if(this._isSubscribing())if(e.code<100||e.code===109||e.temporary===!0){e.code===109&&(this._token="");let t={channel:this.channel,type:"subscribe",error:e};this._centrifuge.state==="connected"&&this.emit("error",t),this._scheduleResubscribe()}else this._setUnsubscribed(e.code,e.message,!1)}_getResubscribeDelay(){let e=S(this._resubscribeAttempts,this._minResubscribeDelay,this._maxResubscribeDelay);return this._resubscribeAttempts++,e}_setOptions(e){e&&(e.since&&(this._offset=e.since.offset,this._epoch=e.since.epoch,this._recover=!0),e.data&&(this._data=e.data),e.getData&&(this._getData=e.getData),e.minResubscribeDelay!==void 0&&(this._minResubscribeDelay=e.minResubscribeDelay),e.maxResubscribeDelay!==void 0&&(this._maxResubscribeDelay=e.maxResubscribeDelay),e.token&&(this._token=e.token),e.getToken&&(this._getToken=e.getToken),e.positioned===!0&&(this._positioned=!0),e.recoverable===!0&&(this._recoverable=!0),e.joinLeave===!0&&(this._joinLeave=!0))}_getOffset(){let e=this._offset;return e!==null?e:0}_getEpoch(){let e=this._epoch;return e!==null?e:""}_clearRefreshTimeout(){this._refreshTimeout!==null&&(clearTimeout(this._refreshTimeout),this._refreshTimeout=null)}_clearResubscribeTimeout(){this._resubscribeTimeout!==null&&(clearTimeout(this._resubscribeTimeout),this._resubscribeTimeout=null)}_getSubscriptionToken(){this._centrifuge._debug("get subscription token for channel",this.channel);let e={channel:this.channel},t=this._getToken;if(t===null)throw this.emit("error",{type:"configuration",channel:this.channel,error:{code:12,message:"provide a function to get channel subscription token"}}),new b("");return t(e)}_refresh(){this._clearRefreshTimeout();let e=this;this._getSubscriptionToken().then(function(t){if(!e._isSubscribed())return;if(!t){e._failUnauthorized();return}e._token=t;let i={sub_refresh:{channel:e.channel,token:t}};e._centrifuge._call(i).then(r=>{let a=r.reply.sub_refresh;e._refreshResponse(a),r.next&&r.next()},r=>{e._refreshError(r.error),r.next&&r.next()})}).catch(function(t){if(t instanceof b){e._failUnauthorized();return}e.emit("error",{type:"refreshToken",channel:e.channel,error:{code:9,message:t!==void 0?t.toString():""}}),e._refreshTimeout=setTimeout(()=>e._refresh(),e._getRefreshRetryDelay())})}_refreshResponse(e){this._isSubscribed()&&(this._centrifuge._debug("subscription token refreshed, channel",this.channel),this._clearRefreshTimeout(),e.expires===!0&&(this._refreshTimeout=setTimeout(()=>this._refresh(),x(e.ttl))))}_refreshError(e){this._isSubscribed()&&(e.code<100||e.temporary===!0?(this.emit("error",{type:"refresh",channel:this.channel,error:e}),this._refreshTimeout=setTimeout(()=>this._refresh(),this._getRefreshRetryDelay())):this._setUnsubscribed(e.code,e.message,!0))}_getRefreshRetryDelay(){return S(0,1e4,2e4)}_failUnauthorized(){this._setUnsubscribed(1,"unauthorized",!0)}};var L=class{constructor(s,e){this.endpoint=s,this.options=e,this._transport=null}name(){return"sockjs"}subName(){return"sockjs-"+this._transport.transport}emulation(){return!1}supported(){return this.options.sockjs!==null}initialize(s,e){this._transport=new this.options.sockjs(this.endpoint,null,this.options.sockjsOptions),this._transport.onopen=()=>{e.onOpen()},this._transport.onerror=t=>{e.onError(t)},this._transport.onclose=t=>{e.onClose(t)},this._transport.onmessage=t=>{e.onMessage(t.data)}}close(){this._transport.close()}send(s){this._transport.send(s)}};var T=class{constructor(s,e){this.endpoint=s,this.options=e,this._transport=null}name(){return"websocket"}subName(){return"websocket"}emulation(){return!1}supported(){return this.options.websocket!==void 0&&this.options.websocket!==null}initialize(s,e){let t="";s==="protobuf"&&(t="centrifuge-protobuf"),t!==""?this._transport=new this.options.websocket(this.endpoint,t):this._transport=new this.options.websocket(this.endpoint),s==="protobuf"&&(this._transport.binaryType="arraybuffer"),this._transport.onopen=()=>{e.onOpen()},this._transport.onerror=n=>{e.onError(n)},this._transport.onclose=n=>{e.onClose(n)},this._transport.onmessage=n=>{e.onMessage(n.data)}}close(){this._transport.close()}send(s){this._transport.send(s)}};var j=class{constructor(s,e){this.endpoint=s,this.options=e,this._abortController=null,this._utf8decoder=new TextDecoder,this._protocol="json"}name(){return"http_stream"}subName(){return"http_stream"}emulation(){return!0}_handleErrors(s){if(!s.ok)throw new Error(s.status);return s}_fetchEventTarget(s,e,t){let n=new EventTarget,i=s.options.fetch;return i(e,t).then(s._handleErrors).then(r=>{n.dispatchEvent(new Event("open"));let a="",c=0,h=new Uint8Array,m=r.body.getReader();return new s.options.readableStream({start(v){function w(){return m.read().then(({done:_,value:l})=>{if(_){n.dispatchEvent(new Event("close")),v.close();return}try{if(s._protocol==="json")for(a+=s._utf8decoder.decode(l);c{n.dispatchEvent(new Event("error",{detail:r})),n.dispatchEvent(new Event("close"))}),n}supported(){return this.options.fetch!==null&&this.options.readableStream!==null&&typeof TextDecoder<"u"&&typeof AbortController<"u"&&typeof EventTarget<"u"&&typeof Event<"u"&&typeof MessageEvent<"u"&&typeof Error<"u"}initialize(s,e,t){this._protocol=s,this._abortController=new AbortController;let n,i;s==="json"?(n={Accept:"application/json","Content-Type":"application/json"},i=t):(n={Accept:"application/octet-stream","Content-Type":"application/octet-stream"},i=t);let r={method:"POST",headers:n,body:i,mode:"cors",credentials:"same-origin",cache:"no-cache",signal:this._abortController.signal},a=this._fetchEventTarget(this,this.endpoint,r);a.addEventListener("open",()=>{e.onOpen()}),a.addEventListener("error",c=>{this._abortController.abort(),e.onError(c)}),a.addEventListener("close",()=>{this._abortController.abort(),e.onClose({code:4,reason:"connection closed"})}),a.addEventListener("message",c=>{e.onMessage(c.data)})}close(){this._abortController.abort()}send(s,e,t){let n,i,r={session:e,node:t,data:s};this._protocol==="json"?(n={"Content-Type":"application/json"},i=JSON.stringify(r)):(n={"Content-Type":"application/octet-stream"},i=this.options.encoder.encodeEmulationRequest(r));let a=this.options.fetch,c={method:"POST",headers:n,body:i,mode:"cors",credentials:"same-origin",cache:"no-cache"};a(this.options.emulationEndpoint,c)}};var D=class{constructor(s,e){this.endpoint=s,this.options=e,this._protocol="json",this._transport=null,this._onClose=null}name(){return"sse"}subName(){return"sse"}emulation(){return!0}supported(){return this.options.eventsource!==null&&this.options.fetch!==null}initialize(s,e,t){let n;globalThis&&globalThis.document&&globalThis.document.baseURI?n=new URL(this.endpoint,globalThis.document.baseURI):n=new URL(this.endpoint),n.searchParams.append("cf_connect",t);let i={},r=new this.options.eventsource(n.toString(),i);this._transport=r;let a=this;r.onopen=function(){e.onOpen()},r.onerror=function(c){r.close(),e.onError(c),e.onClose({code:4,reason:"connection closed"})},r.onmessage=function(c){e.onMessage(c.data)},a._onClose=function(){e.onClose({code:4,reason:"connection closed"})}}close(){this._transport.close(),this._onClose!==null&&this._onClose()}send(s,e,t){let n={session:e,node:t,data:s},i={"Content-Type":"application/json"},r=JSON.stringify(n),a=this.options.fetch,c={method:"POST",headers:i,body:r,mode:"cors",credentials:"same-origin",cache:"no-cache"};a(this.options.emulationEndpoint,c)}};var I=class{constructor(s,e){this.endpoint=s,this.options=e,this._transport=null,this._stream=null,this._writer=null,this._utf8decoder=new TextDecoder,this._protocol="json"}name(){return"webtransport"}subName(){return"webtransport"}emulation(){return!1}supported(){return this.options.webtransport!==void 0&&this.options.webtransport!==null}async initialize(s,e){let t;globalThis&&globalThis.document&&globalThis.document.baseURI?t=new URL(this.endpoint,globalThis.document.baseURI):t=new URL(this.endpoint),s==="protobuf"&&t.searchParams.append("cf_protocol","protobuf"),this._protocol=s;let n=new EventTarget;this._transport=new this.options.webtransport(t.toString()),this._transport.closed.then(()=>{e.onClose({code:4,reason:"connection closed"})}).catch(()=>{e.onClose({code:4,reason:"connection closed"})});try{await this._transport.ready}catch{this.close();return}let i;try{i=await this._transport.createBidirectionalStream()}catch{this.close();return}this._stream=i,this._writer=this._stream.writable.getWriter(),n.addEventListener("close",()=>{e.onClose({code:4,reason:"connection closed"})}),n.addEventListener("message",r=>{e.onMessage(r.data)}),this._startReading(n),e.onOpen()}async _startReading(s){let e=this._stream.readable.getReader(),t="",n=0,i=new Uint8Array;try{for(;;){let{done:r,value:a}=await e.read();if(a.length>0)if(this._protocol==="json")for(t+=this._utf8decoder.decode(a);nJSON.stringify(e)).join(` +`)}decodeReplies(s){return s.trim().split(` +`).map(e=>JSON.parse(e))}};var te=W(M()),ve={token:"",getToken:null,data:null,getData:null,debug:!1,name:"js",version:"",fetch:null,readableStream:null,websocket:null,eventsource:null,sockjs:null,sockjsOptions:{},emulationEndpoint:"/emulation",minReconnectDelay:500,maxReconnectDelay:2e4,timeout:5e3,maxServerPingDelay:1e4,networkEventTarget:null},b=class extends Error{constructor(s){super(s),this.name=this.constructor.name}},g=class extends te.default{constructor(e,t){super();this._reconnectTimeout=null;this._refreshTimeout=null;this._serverPingTimeout=null;this.state="disconnected",this._transportIsOpen=!1,this._endpoint=e,this._emulation=!1,this._transports=[],this._currentTransportIndex=0,this._triedAllTransports=!1,this._transportWasOpen=!1,this._transport=null,this._transportId=0,this._deviceWentOffline=!1,this._transportClosed=!0,this._codec=new E,this._reconnecting=!1,this._reconnectTimeout=null,this._reconnectAttempts=0,this._client=null,this._session="",this._node="",this._subs={},this._serverSubs={},this._commandId=0,this._commands=[],this._batching=!1,this._refreshRequired=!1,this._refreshTimeout=null,this._callbacks={},this._token="",this._data=null,this._dispatchPromise=Promise.resolve(),this._serverPing=0,this._serverPingTimeout=null,this._sendPong=!1,this._promises={},this._promiseId=0,this._debugEnabled=!1,this._networkEventsSet=!1,this._config={...ve,...t},this._configure(),this._debugEnabled?(this.on("state",n=>{this._debug("client state",n.oldState,"->",n.newState)}),this.on("error",n=>{this._debug("client error",n)})):this.on("error",function(){Function.prototype()})}newSubscription(e,t){if(this.getSubscription(e)!==null)throw new Error("Subscription to the channel "+e+" already exists");let n=new O(this,e,t);return this._subs[e]=n,n}getSubscription(e){return this._getSub(e)}removeSubscription(e){e&&(e.state!=="unsubscribed"&&e.unsubscribe(),this._removeSubscription(e))}subscriptions(){return this._subs}ready(e){return this.state==="disconnected"?Promise.reject({code:3,message:"client disconnected"}):this.state==="connected"?Promise.resolve():new Promise((t,n)=>{let i={resolve:t,reject:n};e&&(i.timeout=setTimeout(function(){n({code:1,message:"timeout"})},e)),this._promises[this._nextPromiseId()]=i})}connect(){if(this._isConnected()){this._debug("connect called when already connected");return}if(this._isConnecting()){this._debug("connect called when already connecting");return}this._debug("connect called"),this._reconnectAttempts=0,this._startConnecting()}disconnect(){this._disconnect(0,"disconnect called",!1)}setToken(e){this._token=e}send(e){let t={send:{data:e}},n=this;return this._methodCall().then(function(){return n._transportSendCommands([t])?Promise.resolve():Promise.reject(n._createErrorObject(10,"transport write error"))})}rpc(e,t){let n={rpc:{method:e,data:t}},i=this;return this._methodCall().then(function(){return i._callPromise(n,function(r){return{data:r.rpc.data}})})}publish(e,t){let n={publish:{channel:e,data:t}},i=this;return this._methodCall().then(function(){return i._callPromise(n,function(){return{}})})}history(e,t){let n={history:this._getHistoryRequest(e,t)},i=this;return this._methodCall().then(function(){return i._callPromise(n,function(r){let a=r.history,c=[];if(a.publications)for(let h=0;h{this._debug("offline event triggered"),(this.state==="connected"||this.state==="connecting")&&(this._disconnect(1,"transport closed",!0),this._deviceWentOffline=!0)}),e.addEventListener("online",()=>{this._debug("online event triggered"),this.state==="connecting"&&(this._deviceWentOffline&&!this._transportClosed&&(this._deviceWentOffline=!1,this._transportClosed=!0),this._clearReconnectTimeout(),this._startReconnecting())}),this._networkEventsSet=!0)}_getReconnectDelay(){let e=S(this._reconnectAttempts,this._config.minReconnectDelay,this._config.maxReconnectDelay);return this._reconnectAttempts+=1,e}_clearOutgoingRequests(){for(let e in this._callbacks)if(this._callbacks.hasOwnProperty(e)){let t=this._callbacks[e];clearTimeout(t.timeout);let n=t.errback;if(!n)continue;n({error:this._createErrorObject(11,"connection closed")})}this._callbacks={}}_clearConnectedState(){this._client=null,this._clearServerPingTimeout(),this._clearRefreshTimeout();for(let e in this._subs){if(!this._subs.hasOwnProperty(e))continue;let t=this._subs[e];t.state==="subscribed"&&t._setSubscribing(1,"transport closed")}for(let e in this._serverSubs)this._serverSubs.hasOwnProperty(e)&&this.emit("subscribing",{channel:e})}_handleWriteError(e){for(let t of e){let n=t.id;if(!(n in this._callbacks))continue;let i=this._callbacks[n];clearTimeout(this._callbacks[n].timeout),delete this._callbacks[n];let r=i.errback;r({error:this._createErrorObject(10,"transport write error")})}}_transportSendCommands(e){if(!e.length)return!0;if(!this._transport)return!1;try{this._transport.send(this._codec.encodeCommands(e),this._session,this._node)}catch(t){return this._debug("error writing commands",t),this._handleWriteError(e),!1}return!0}_initializeTransport(){let e;this._config.websocket!==null?e=this._config.websocket:typeof globalThis.WebSocket!="function"&&typeof globalThis.WebSocket!="object"||(e=globalThis.WebSocket);let t=null;this._config.sockjs!==null?t=this._config.sockjs:typeof globalThis.SockJS<"u"&&(t=globalThis.SockJS);let n=null;this._config.eventsource!==null?n=this._config.eventsource:typeof globalThis.EventSource<"u"&&(n=globalThis.EventSource);let i=null;this._config.fetch!==null?i=this._config.fetch:typeof globalThis.fetch<"u"&&(i=globalThis.fetch);let r=null;if(this._config.readableStream!==null?r=this._config.readableStream:typeof globalThis.ReadableStream<"u"&&(r=globalThis.ReadableStream),this._emulation){this._currentTransportIndex>=this._transports.length&&(this._triedAllTransports=!0,this._currentTransportIndex=0);let l=0;for(;;){if(l>=this._transports.length)throw new Error("no supported transport found");let p=this._transports[this._currentTransportIndex],d=p.transport,f=p.endpoint;if(d==="websocket"){if(this._debug("trying websocket transport"),this._transport=new T(f,{websocket:e}),!this._transport.supported()){this._debug("websocket transport not available"),this._currentTransportIndex++,l++;continue}}else if(d==="webtransport"){if(this._debug("trying webtransport transport"),this._transport=new I(f,{webtransport:globalThis.WebTransport,decoder:this._codec,encoder:this._codec}),!this._transport.supported()){this._debug("webtransport transport not available"),this._currentTransportIndex++,l++;continue}}else if(d==="http_stream"){if(this._debug("trying http_stream transport"),this._transport=new j(f,{fetch:i,readableStream:r,emulationEndpoint:this._config.emulationEndpoint,decoder:this._codec,encoder:this._codec}),!this._transport.supported()){this._debug("http_stream transport not available"),this._currentTransportIndex++,l++;continue}}else if(d==="sse"){if(this._debug("trying sse transport"),this._transport=new D(f,{eventsource:n,fetch:i,emulationEndpoint:this._config.emulationEndpoint}),!this._transport.supported()){this._debug("sse transport not available"),this._currentTransportIndex++,l++;continue}}else if(d==="sockjs"){if(this._debug("trying sockjs"),this._transport=new L(f,{sockjs:t,sockjsOptions:this._config.sockjsOptions}),!this._transport.supported()){this._debug("sockjs transport not available"),this._currentTransportIndex++,l++;continue}}else throw new Error("unknown transport "+d);break}}else{if(Y(this._endpoint,"http"))throw new Error("Provide explicit transport endpoints configuration in case of using HTTP (i.e. using array of TransportEndpoint instead of a single string), or use ws(s):// scheme in an endpoint if you aimed using WebSocket transport");if(this._debug("client will use websocket"),this._transport=new T(this._endpoint,{websocket:e}),!this._transport.supported())throw new Error("WebSocket not available")}let a=this,c=this._transport,h=this._nextTransportId();a._debug("id of transport",h);let m=!1,v=[];if(this._transport.emulation()){let l=a._sendConnect(!0);v.push(l)}this._setNetworkEvents();let w=this._codec.encodeCommands(v);this._transportClosed=!1;let _;_=setTimeout(function(){c.close()},this._config.timeout),this._transport.initialize(this._codec.name(),{onOpen:function(){if(_&&(clearTimeout(_),_=null),a._transportId!=h){a._debug("open callback from non-actual transport"),c.close();return}m=!0,a._debug(c.subName(),"transport open"),!c.emulation()&&(a._transportIsOpen=!0,a._transportWasOpen=!0,a.startBatching(),a._sendConnect(!1),a._sendSubscribeCommands(),a.stopBatching(),a.emit("__centrifuge_debug:connect_frame_sent",{}))},onError:function(l){if(a._transportId!=h){a._debug("error callback from non-actual transport");return}a._debug("transport level error",l)},onClose:function(l){if(_&&(clearTimeout(_),_=null),a._transportId!=h){a._debug("close callback from non-actual transport");return}a._debug(c.subName(),"transport closed"),a._transportClosed=!0,a._transportIsOpen=!1;let p="connection closed",d=!0,f=0;if(l&&"code"in l&&l.code&&(f=l.code),l&&l.reason)try{let U=JSON.parse(l.reason);p=U.reason,d=U.reconnect}catch{p=l.reason,(f>=3500&&f<4e3||f>=4500&&f<5e3)&&(d=!1)}f<3e3?(f===1009?(f=3,p="message size limit exceeded",d=!1):(f=1,p="transport closed"),a._emulation&&!a._transportWasOpen&&(a._currentTransportIndex++,a._currentTransportIndex>=a._transports.length&&(a._triedAllTransports=!0,a._currentTransportIndex=0))):a._transportWasOpen=!0,a._isConnecting()&&!m&&a.emit("error",{type:"transport",error:{code:2,message:"transport closed"},transport:c.name()}),a._reconnecting=!1,a._disconnect(f,p,d)},onMessage:function(l){a._dataReceived(l)}},w),a.emit("__centrifuge_debug:transport_initialized",{})}_sendConnect(e){let t=this._constructConnectCommand(),n=this;return this._call(t,e).then(i=>{let r=i.reply.connect;n._connectResponse(r),i.next&&i.next()},i=>{n._connectError(i.error),i.next&&i.next()}),t}_startReconnecting(){if(this._debug("start reconnecting"),!this._isConnecting()){this._debug("stop reconnecting: client not in connecting state");return}if(this._reconnecting){this._debug("reconnect already in progress, return from reconnect routine");return}if(this._transportClosed===!1){this._debug("waiting for transport close");return}this._reconnecting=!0;let e=this,t=this._token==="";if(!(this._refreshRequired||t&&this._config.getToken!==null)){this._config.getData?this._config.getData().then(function(i){e._isConnecting()&&(e._data=i,e._initializeTransport())}):this._initializeTransport();return}this._getToken().then(function(i){if(e._isConnecting()){if(i==null||i==null){e._failUnauthorized();return}e._token=i,e._debug("connection token refreshed"),e._config.getData?e._config.getData().then(function(r){e._isConnecting()&&(e._data=r,e._initializeTransport())}):e._initializeTransport()}}).catch(function(i){if(!e._isConnecting())return;if(i instanceof b){e._failUnauthorized();return}e.emit("error",{type:"connectToken",error:{code:5,message:i!==void 0?i.toString():""}});let r=e._getReconnectDelay();e._debug("error on connection token refresh, reconnect after "+r+" milliseconds",i),e._reconnecting=!1,e._reconnectTimeout=setTimeout(()=>{e._startReconnecting()},r)})}_connectError(e){this.state==="connecting"&&(e.code===109&&(this._refreshRequired=!0),e.code<100||e.temporary===!0||e.code===109?(this.emit("error",{type:"connect",error:e}),this._debug("closing transport due to connect error"),this._disconnect(e.code,e.message,!0)):this._disconnect(e.code,e.message,!1))}_scheduleReconnect(){if(!this._isConnecting())return;let e=!1;this._emulation&&!this._transportWasOpen&&!this._triedAllTransports&&(e=!0);let t=this._getReconnectDelay();e&&(t=0),this._debug("reconnect after "+t+" milliseconds"),this._clearReconnectTimeout(),this._reconnectTimeout=setTimeout(()=>{this._startReconnecting()},t)}_constructConnectCommand(){let e={};this._token&&(e.token=this._token),this._data&&(e.data=this._data),this._config.name&&(e.name=this._config.name),this._config.version&&(e.version=this._config.version);let t={},n=!1;for(let i in this._serverSubs)if(this._serverSubs.hasOwnProperty(i)&&this._serverSubs[i].recoverable){n=!0;let r={recover:!0};this._serverSubs[i].offset&&(r.offset=this._serverSubs[i].offset),this._serverSubs[i].epoch&&(r.epoch=this._serverSubs[i].epoch),t[i]=r}return n&&(e.subs=t),{connect:e}}_getHistoryRequest(e,t){let n={channel:e};return t!==void 0&&(t.since&&(n.since={offset:t.since.offset},t.since.epoch&&(n.since.epoch=t.since.epoch)),t.limit!==void 0&&(n.limit=t.limit),t.reverse===!0&&(n.reverse=!0)),n}_methodCall(){return this._isConnected()?Promise.resolve():new Promise((e,t)=>{let n=setTimeout(function(){t({code:1,message:"timeout"})},this._config.timeout);this._promises[this._nextPromiseId()]={timeout:n,resolve:e,reject:t}})}_callPromise(e,t){return new Promise((n,i)=>{this._call(e,!1).then(r=>{n(t(r.reply)),r.next&&r.next()},r=>{i(r.error),r.next&&r.next()})})}_dataReceived(e){this._serverPing>0&&this._waitServerPing();let t=this._codec.decodeReplies(e);this._dispatchPromise=this._dispatchPromise.then(()=>{let n;this._dispatchPromise=new Promise(i=>{n=i}),this._dispatchSynchronized(t,n)})}_dispatchSynchronized(e,t){let n=Promise.resolve();for(let i in e)e.hasOwnProperty(i)&&(n=n.then(()=>this._dispatchReply(e[i])));n=n.then(()=>{t()})}_dispatchReply(e){let t,n=new Promise(r=>{t=r});if(e==null)return this._debug("dispatch: got undefined or null reply"),t(),n;let i=e.id;return i&&i>0?this._handleReply(e,t):e.push?this._handlePush(e.push,t):this._handleServerPing(t),n}_call(e,t){return new Promise((n,i)=>{e.id=this._nextCommandId(),this._registerCall(e.id,n,i),t||this._addCommand(e)})}_startConnecting(){this._debug("start connecting"),this._setState("connecting")&&this.emit("connecting",{code:0,reason:"connect called"}),this._client=null,this._startReconnecting()}_disconnect(e,t,n){if(this._isDisconnected())return;this._transportIsOpen=!1;let i=this.state;this._reconnecting=!1;let r={code:e,reason:t},a=!1;if(n?a=this._setState("connecting"):(a=this._setState("disconnected"),this._rejectPromises({code:3,message:"disconnected"})),this._clearOutgoingRequests(),i==="connecting"&&this._clearReconnectTimeout(),i==="connected"&&this._clearConnectedState(),a&&(this._isConnecting()?this.emit("connecting",r):this.emit("disconnected",r)),this._transport){this._debug("closing existing transport");let c=this._transport;this._transport=null,c.close(),this._transportClosed=!0,this._nextTransportId()}else this._debug("no transport to close");this._scheduleReconnect()}_failUnauthorized(){this._disconnect(1,"unauthorized",!1)}_getToken(){if(this._debug("get connection token"),!this._config.getToken)throw this.emit("error",{type:"configuration",error:{code:12,message:"token expired but no getToken function set in the configuration"}}),new b("");return this._config.getToken({})}_refresh(){let e=this._client,t=this;this._getToken().then(function(n){if(e!==t._client)return;if(!n){t._failUnauthorized();return}if(t._token=n,t._debug("connection token refreshed"),!t._isConnected())return;let i={refresh:{token:t._token}};t._call(i,!1).then(r=>{let a=r.reply.refresh;t._refreshResponse(a),r.next&&r.next()},r=>{t._refreshError(r.error),r.next&&r.next()})}).catch(function(n){if(t._isConnected()){if(n instanceof b){t._failUnauthorized();return}t.emit("error",{type:"refreshToken",error:{code:6,message:n!==void 0?n.toString():""}}),t._refreshTimeout=setTimeout(()=>t._refresh(),t._getRefreshRetryDelay())}})}_refreshError(e){e.code<100||e.temporary===!0?(this.emit("error",{type:"refresh",error:e}),this._refreshTimeout=setTimeout(()=>this._refresh(),this._getRefreshRetryDelay())):this._disconnect(e.code,e.message,!1)}_getRefreshRetryDelay(){return S(0,5e3,1e4)}_refreshResponse(e){this._refreshTimeout&&(clearTimeout(this._refreshTimeout),this._refreshTimeout=null),e.expires&&(this._client=e.client,this._refreshTimeout=setTimeout(()=>this._refresh(),x(e.ttl)))}_removeSubscription(e){e!==null&&delete this._subs[e.channel]}_unsubscribe(e){if(!this._transportIsOpen)return;let n={unsubscribe:{channel:e.channel}},i=this;this._call(n,!1).then(r=>{r.next&&r.next()},r=>{r.next&&r.next(),i._disconnect(4,"unsubscribe error",!0)})}_getSub(e){let t=this._subs[e];return t||null}_isServerSub(e){return this._serverSubs[e]!==void 0}_sendSubscribeCommands(){let e=[];for(let t in this._subs){if(!this._subs.hasOwnProperty(t))continue;let n=this._subs[t];if(n._inflight!==!0&&n.state==="subscribing"){let i=n._subscribe();i&&e.push(i)}}return e}_connectResponse(e){if(this._transportIsOpen=!0,this._transportWasOpen=!0,this._reconnectAttempts=0,this._refreshRequired=!1,this._isConnected())return;this._client=e.client,this._setState("connected"),this._refreshTimeout&&clearTimeout(this._refreshTimeout),e.expires&&(this._refreshTimeout=setTimeout(()=>this._refresh(),x(e.ttl))),this._session=e.session,this._node=e.node,this.startBatching(),this._sendSubscribeCommands(),this.stopBatching();let t={client:e.client,transport:this._transport.subName()};e.data&&(t.data=e.data),this.emit("connected",t),this._resolvePromises(),this._processServerSubs(e.subs||{}),e.ping&&e.ping>0?(this._serverPing=e.ping*1e3,this._sendPong=e.pong===!0,this._waitServerPing()):this._serverPing=0}_processServerSubs(e){for(let t in e){if(!e.hasOwnProperty(t))continue;let n=e[t];this._serverSubs[t]={offset:n.offset,epoch:n.epoch,recoverable:n.recoverable||!1};let i=this._getSubscribeContext(t,n);this.emit("subscribed",i)}for(let t in e){if(!e.hasOwnProperty(t))continue;let n=e[t];if(n.recovered){let i=n.publications;if(i&&i.length>0)for(let r in i)i.hasOwnProperty(r)&&this._handlePublication(t,i[r])}}for(let t in this._serverSubs)this._serverSubs.hasOwnProperty(t)&&(e[t]||(this.emit("unsubscribed",{channel:t}),delete this._serverSubs[t]))}_clearRefreshTimeout(){this._refreshTimeout!==null&&(clearTimeout(this._refreshTimeout),this._refreshTimeout=null)}_clearReconnectTimeout(){this._reconnectTimeout!==null&&(clearTimeout(this._reconnectTimeout),this._reconnectTimeout=null)}_clearServerPingTimeout(){this._serverPingTimeout!==null&&(clearTimeout(this._serverPingTimeout),this._serverPingTimeout=null)}_waitServerPing(){this._config.maxServerPingDelay!==0&&this._isConnected()&&(this._clearServerPingTimeout(),this._serverPingTimeout=setTimeout(()=>{this._isConnected()&&this._disconnect(2,"no ping",!0)},this._serverPing+this._config.maxServerPingDelay))}_getSubscribeContext(e,t){let n={channel:e,positioned:!1,recoverable:!1,wasRecovering:!1,recovered:!1};t.recovered&&(n.recovered=!0),t.positioned&&(n.positioned=!0),t.recoverable&&(n.recoverable=!0),t.was_recovering&&(n.wasRecovering=!0);let i="";"epoch"in t&&(i=t.epoch);let r=0;return"offset"in t&&(r=t.offset),(n.positioned||n.recoverable)&&(n.streamPosition={offset:r,epoch:i}),t.data&&(n.data=t.data),n}_handleReply(e,t){let n=e.id;if(!(n in this._callbacks)){t();return}let i=this._callbacks[n];if(clearTimeout(this._callbacks[n].timeout),delete this._callbacks[n],$(e)){let r=i.errback;if(!r){t();return}let a=e.error;r({error:a,next:t})}else{let r=i.callback;if(!r)return;r({reply:e,next:t})}}_handleJoin(e,t){let n=this._getSub(e);if(!n){if(this._isServerSub(e)){let i={channel:e,info:this._getJoinLeaveContext(t.info)};this.emit("join",i)}return}n._handleJoin(t)}_handleLeave(e,t){let n=this._getSub(e);if(!n){if(this._isServerSub(e)){let i={channel:e,info:this._getJoinLeaveContext(t.info)};this.emit("leave",i)}return}n._handleLeave(t)}_handleUnsubscribe(e,t){let n=this._getSub(e);if(!n){this._isServerSub(e)&&(delete this._serverSubs[e],this.emit("unsubscribed",{channel:e}));return}t.code<2500?n._setUnsubscribed(t.code,t.reason,!1):n._setSubscribing(t.code,t.reason)}_handleSubscribe(e,t){this._serverSubs[e]={offset:t.offset,epoch:t.epoch,recoverable:t.recoverable||!1},this.emit("subscribed",this._getSubscribeContext(e,t))}_handleDisconnect(e){let t=e.code,n=!0;(t>=3500&&t<4e3||t>=4500&&t<5e3)&&(n=!1),this._disconnect(t,e.reason,n)}_getPublicationContext(e,t){let n={channel:e,data:t.data};return t.offset&&(n.offset=t.offset),t.info&&(n.info=this._getJoinLeaveContext(t.info)),t.tags&&(n.tags=t.tags),n}_getJoinLeaveContext(e){let t={client:e.client,user:e.user};return e.conn_info&&(t.connInfo=e.conn_info),e.chan_info&&(t.chanInfo=e.chan_info),t}_handlePublication(e,t){let n=this._getSub(e);if(!n){if(this._isServerSub(e)){let i=this._getPublicationContext(e,t);this.emit("publication",i),t.offset!==void 0&&(this._serverSubs[e].offset=t.offset)}return}n._handlePublication(t)}_handleMessage(e){this.emit("message",{data:e.data})}_handleServerPing(e){if(this._sendPong){let t={};this._transportSendCommands([t])}e()}_handlePush(e,t){let n=e.channel;e.pub?this._handlePublication(n,e.pub):e.message?this._handleMessage(e.message):e.join?this._handleJoin(n,e.join):e.leave?this._handleLeave(n,e.leave):e.unsubscribe?this._handleUnsubscribe(n,e.unsubscribe):e.subscribe?this._handleSubscribe(n,e.subscribe):e.disconnect&&this._handleDisconnect(e.disconnect),t()}_flush(){let e=this._commands.slice(0);this._commands=[],this._transportSendCommands(e)}_createErrorObject(e,t,n){let i={code:e,message:t};return n&&(i.temporary=!0),i}_registerCall(e,t,n){this._callbacks[e]={callback:t,errback:n,timeout:null},this._callbacks[e].timeout=setTimeout(()=>{delete this._callbacks[e],z(n)&&n({error:this._createErrorObject(1,"timeout")})},this._config.timeout)}_addCommand(e){this._batching?this._commands.push(e):this._transportSendCommands([e])}_nextPromiseId(){return++this._promiseId}_nextTransportId(){return++this._transportId}_resolvePromises(){for(let e in this._promises)this._promises.hasOwnProperty(e)&&(this._promises[e].timeout&&clearTimeout(this._promises[e].timeout),this._promises[e].resolve(),delete this._promises[e])}_rejectPromises(e){for(let t in this._promises)this._promises.hasOwnProperty(t)&&(this._promises[t].timeout&&clearTimeout(this._promises[t].timeout),this._promises[t].reject(e),delete this._promises[t])}};g.SubscriptionState=R;g.State=k;g.UnauthorizedError=b;globalThis.Centrifuge=g;})(); +//# sourceMappingURL=centrifuge.js.map diff --git a/_examples/recovery_mode_cache/d3.v7.min.js b/_examples/recovery_mode_cache/d3.v7.min.js new file mode 100644 index 00000000..33bb8802 --- /dev/null +++ b/_examples/recovery_mode_cache/d3.v7.min.js @@ -0,0 +1,2 @@ +// https://d3js.org v7.9.0 Copyright 2010-2023 Mike Bostock +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";function n(t,n){return null==t||null==n?NaN:tn?1:t>=n?0:NaN}function e(t,n){return null==t||null==n?NaN:nt?1:n>=t?0:NaN}function r(t){let r,o,a;function u(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<0?e=r+1:i=r}while(en(t(e),r),a=(n,e)=>t(n)-e):(r=t===n||t===e?t:i,o=t,a=t),{left:u,center:function(t,n,e=0,r=t.length){const i=u(t,n,e,r-1);return i>e&&a(t[i-1],n)>-a(t[i],n)?i-1:i},right:function(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<=0?e=r+1:i=r}while(e{n(t,e,(r<<=2)+0,(i<<=2)+0,o<<=2),n(t,e,r+1,i+1,o),n(t,e,r+2,i+2,o),n(t,e,r+3,i+3,o)}}));function d(t){return function(n,e,r=e){if(!((e=+e)>=0))throw new RangeError("invalid rx");if(!((r=+r)>=0))throw new RangeError("invalid ry");let{data:i,width:o,height:a}=n;if(!((o=Math.floor(o))>=0))throw new RangeError("invalid width");if(!((a=Math.floor(void 0!==a?a:i.length/o))>=0))throw new RangeError("invalid height");if(!o||!a||!e&&!r)return n;const u=e&&t(e),c=r&&t(r),f=i.slice();return u&&c?(p(u,f,i,o,a),p(u,i,f,o,a),p(u,f,i,o,a),g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)):u?(p(u,i,f,o,a),p(u,f,i,o,a),p(u,i,f,o,a)):c&&(g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)),n}}function p(t,n,e,r,i){for(let o=0,a=r*i;o{if(!((o-=a)>=i))return;let u=t*r[i];const c=a*t;for(let t=i,n=i+c;t{if(!((a-=u)>=o))return;let c=n*i[o];const f=u*n,s=f+u;for(let t=o,n=o+f;t=n&&++e;else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(i=+i)>=i&&++e}return e}function _(t){return 0|t.length}function b(t){return!(t>0)}function m(t){return"object"!=typeof t||"length"in t?t:Array.from(t)}function x(t,n){let e,r=0,i=0,o=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(e=n-i,i+=e/++r,o+=e*(n-i));else{let a=-1;for(let u of t)null!=(u=n(u,++a,t))&&(u=+u)>=u&&(e=u-i,i+=e/++r,o+=e*(u-i))}if(r>1)return o/(r-1)}function w(t,n){const e=x(t,n);return e?Math.sqrt(e):e}function M(t,n){let e,r;if(void 0===n)for(const n of t)null!=n&&(void 0===e?n>=n&&(e=r=n):(e>n&&(e=n),r=o&&(e=r=o):(e>o&&(e=o),r0){for(o=t[--i];i>0&&(n=o,e=t[--i],o=n+e,r=e-(o-n),!r););i>0&&(r<0&&t[i-1]<0||r>0&&t[i-1]>0)&&(e=2*r,n=o+e,e==n-o&&(o=n))}return o}}class InternMap extends Map{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const[n,e]of t)this.set(n,e)}get(t){return super.get(A(this,t))}has(t){return super.has(A(this,t))}set(t,n){return super.set(S(this,t),n)}delete(t){return super.delete(E(this,t))}}class InternSet extends Set{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const n of t)this.add(n)}has(t){return super.has(A(this,t))}add(t){return super.add(S(this,t))}delete(t){return super.delete(E(this,t))}}function A({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):e}function S({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):(t.set(r,e),e)}function E({_intern:t,_key:n},e){const r=n(e);return t.has(r)&&(e=t.get(r),t.delete(r)),e}function N(t){return null!==t&&"object"==typeof t?t.valueOf():t}function k(t){return t}function C(t,...n){return F(t,k,k,n)}function P(t,...n){return F(t,Array.from,k,n)}function z(t,n){for(let e=1,r=n.length;et.pop().map((([n,e])=>[...t,n,e]))));return t}function $(t,n,...e){return F(t,k,n,e)}function D(t,n,...e){return F(t,Array.from,n,e)}function R(t){if(1!==t.length)throw new Error("duplicate key");return t[0]}function F(t,n,e,r){return function t(i,o){if(o>=r.length)return e(i);const a=new InternMap,u=r[o++];let c=-1;for(const t of i){const n=u(t,++c,i),e=a.get(n);e?e.push(t):a.set(n,[t])}for(const[n,e]of a)a.set(n,t(e,o));return n(a)}(t,0)}function q(t,n){return Array.from(n,(n=>t[n]))}function U(t,...n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");t=Array.from(t);let[e]=n;if(e&&2!==e.length||n.length>1){const r=Uint32Array.from(t,((t,n)=>n));return n.length>1?(n=n.map((n=>t.map(n))),r.sort(((t,e)=>{for(const r of n){const n=O(r[t],r[e]);if(n)return n}}))):(e=t.map(e),r.sort(((t,n)=>O(e[t],e[n])))),q(t,r)}return t.sort(I(e))}function I(t=n){if(t===n)return O;if("function"!=typeof t)throw new TypeError("compare is not a function");return(n,e)=>{const r=t(n,e);return r||0===r?r:(0===t(e,e))-(0===t(n,n))}}function O(t,n){return(null==t||!(t>=t))-(null==n||!(n>=n))||(tn?1:0)}var B=Array.prototype.slice;function Y(t){return()=>t}const L=Math.sqrt(50),j=Math.sqrt(10),H=Math.sqrt(2);function X(t,n,e){const r=(n-t)/Math.max(0,e),i=Math.floor(Math.log10(r)),o=r/Math.pow(10,i),a=o>=L?10:o>=j?5:o>=H?2:1;let u,c,f;return i<0?(f=Math.pow(10,-i)/a,u=Math.round(t*f),c=Math.round(n*f),u/fn&&--c,f=-f):(f=Math.pow(10,i)*a,u=Math.round(t/f),c=Math.round(n/f),u*fn&&--c),c0))return[];if((t=+t)===(n=+n))return[t];const r=n=i))return[];const u=o-i+1,c=new Array(u);if(r)if(a<0)for(let t=0;t0?(t=Math.floor(t/i)*i,n=Math.ceil(n/i)*i):i<0&&(t=Math.ceil(t*i)/i,n=Math.floor(n*i)/i),r=i}}function K(t){return Math.max(1,Math.ceil(Math.log(v(t))/Math.LN2)+1)}function Q(){var t=k,n=M,e=K;function r(r){Array.isArray(r)||(r=Array.from(r));var i,o,a,u=r.length,c=new Array(u);for(i=0;i=h)if(t>=h&&n===M){const t=V(l,h,e);isFinite(t)&&(t>0?h=(Math.floor(h/t)+1)*t:t<0&&(h=(Math.ceil(h*-t)+1)/-t))}else d.pop()}for(var p=d.length,g=0,y=p;d[g]<=l;)++g;for(;d[y-1]>h;)--y;(g||y0?d[i-1]:l,v.x1=i0)for(i=0;i=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e=i)&&(e=i)}return e}function tt(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e=o)&&(e=o,r=i);return r}function nt(t,n){let e;if(void 0===n)for(const n of t)null!=n&&(e>n||void 0===e&&n>=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e>i||void 0===e&&i>=i)&&(e=i)}return e}function et(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e>n||void 0===e&&n>=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e>o||void 0===e&&o>=o)&&(e=o,r=i);return r}function rt(t,n,e=0,r=1/0,i){if(n=Math.floor(n),e=Math.floor(Math.max(0,e)),r=Math.floor(Math.min(t.length-1,r)),!(e<=n&&n<=r))return t;for(i=void 0===i?O:I(i);r>e;){if(r-e>600){const o=r-e+1,a=n-e+1,u=Math.log(o),c=.5*Math.exp(2*u/3),f=.5*Math.sqrt(u*c*(o-c)/o)*(a-o/2<0?-1:1);rt(t,n,Math.max(e,Math.floor(n-a*c/o+f)),Math.min(r,Math.floor(n+(o-a)*c/o+f)),i)}const o=t[n];let a=e,u=r;for(it(t,e,n),i(t[r],o)>0&&it(t,e,r);a0;)--u}0===i(t[e],o)?it(t,e,u):(++u,it(t,u,r)),u<=n&&(e=u+1),n<=u&&(r=u-1)}return t}function it(t,n,e){const r=t[n];t[n]=t[e],t[e]=r}function ot(t,e=n){let r,i=!1;if(1===e.length){let o;for(const a of t){const t=e(a);(i?n(t,o)>0:0===n(t,t))&&(r=a,o=t,i=!0)}}else for(const n of t)(i?e(n,r)>0:0===e(n,n))&&(r=n,i=!0);return r}function at(t,n,e){if(t=Float64Array.from(function*(t,n){if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(yield n);else{let e=-1;for(let r of t)null!=(r=n(r,++e,t))&&(r=+r)>=r&&(yield r)}}(t,e)),(r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return nt(t);if(n>=1)return J(t);var r,i=(r-1)*n,o=Math.floor(i),a=J(rt(t,o).subarray(0,o+1));return a+(nt(t.subarray(o+1))-a)*(i-o)}}function ut(t,n,e=o){if((r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return+e(t[0],0,t);if(n>=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,a=Math.floor(i),u=+e(t[a],a,t);return u+(+e(t[a+1],a+1,t)-u)*(i-a)}}function ct(t,n,e=o){if(!isNaN(n=+n)){if(r=Float64Array.from(t,((n,r)=>o(e(t[r],r,t)))),n<=0)return et(r);if(n>=1)return tt(r);var r,i=Uint32Array.from(t,((t,n)=>n)),a=r.length-1,u=Math.floor(a*n);return rt(i,u,0,a,((t,n)=>O(r[t],r[n]))),(u=ot(i.subarray(0,u+1),(t=>r[t])))>=0?u:-1}}function ft(t){return Array.from(function*(t){for(const n of t)yield*n}(t))}function st(t,n){return[t,n]}function lt(t,n,e){t=+t,n=+n,e=(i=arguments.length)<2?(n=t,t=0,1):i<3?1:+e;for(var r=-1,i=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(i);++r+t(n)}function kt(t,n){return n=Math.max(0,t.bandwidth()-2*n)/2,t.round()&&(n=Math.round(n)),e=>+t(e)+n}function Ct(){return!this.__axis}function Pt(t,n){var e=[],r=null,i=null,o=6,a=6,u=3,c="undefined"!=typeof window&&window.devicePixelRatio>1?0:.5,f=t===xt||t===Tt?-1:1,s=t===Tt||t===wt?"x":"y",l=t===xt||t===Mt?St:Et;function h(h){var d=null==r?n.ticks?n.ticks.apply(n,e):n.domain():r,p=null==i?n.tickFormat?n.tickFormat.apply(n,e):mt:i,g=Math.max(o,0)+u,y=n.range(),v=+y[0]+c,_=+y[y.length-1]+c,b=(n.bandwidth?kt:Nt)(n.copy(),c),m=h.selection?h.selection():h,x=m.selectAll(".domain").data([null]),w=m.selectAll(".tick").data(d,n).order(),M=w.exit(),T=w.enter().append("g").attr("class","tick"),A=w.select("line"),S=w.select("text");x=x.merge(x.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),w=w.merge(T),A=A.merge(T.append("line").attr("stroke","currentColor").attr(s+"2",f*o)),S=S.merge(T.append("text").attr("fill","currentColor").attr(s,f*g).attr("dy",t===xt?"0em":t===Mt?"0.71em":"0.32em")),h!==m&&(x=x.transition(h),w=w.transition(h),A=A.transition(h),S=S.transition(h),M=M.transition(h).attr("opacity",At).attr("transform",(function(t){return isFinite(t=b(t))?l(t+c):this.getAttribute("transform")})),T.attr("opacity",At).attr("transform",(function(t){var n=this.parentNode.__axis;return l((n&&isFinite(n=n(t))?n:b(t))+c)}))),M.remove(),x.attr("d",t===Tt||t===wt?a?"M"+f*a+","+v+"H"+c+"V"+_+"H"+f*a:"M"+c+","+v+"V"+_:a?"M"+v+","+f*a+"V"+c+"H"+_+"V"+f*a:"M"+v+","+c+"H"+_),w.attr("opacity",1).attr("transform",(function(t){return l(b(t)+c)})),A.attr(s+"2",f*o),S.attr(s,f*g).text(p),m.filter(Ct).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",t===wt?"start":t===Tt?"end":"middle"),m.each((function(){this.__axis=b}))}return h.scale=function(t){return arguments.length?(n=t,h):n},h.ticks=function(){return e=Array.from(arguments),h},h.tickArguments=function(t){return arguments.length?(e=null==t?[]:Array.from(t),h):e.slice()},h.tickValues=function(t){return arguments.length?(r=null==t?null:Array.from(t),h):r&&r.slice()},h.tickFormat=function(t){return arguments.length?(i=t,h):i},h.tickSize=function(t){return arguments.length?(o=a=+t,h):o},h.tickSizeInner=function(t){return arguments.length?(o=+t,h):o},h.tickSizeOuter=function(t){return arguments.length?(a=+t,h):a},h.tickPadding=function(t){return arguments.length?(u=+t,h):u},h.offset=function(t){return arguments.length?(c=+t,h):c},h}var zt={value:()=>{}};function $t(){for(var t,n=0,e=arguments.length,r={};n=0&&(n=t.slice(e+1),t=t.slice(0,e)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))),a=-1,u=o.length;if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++a0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Ut.hasOwnProperty(n)?{space:Ut[n],local:t}:t}function Ot(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===qt&&n.documentElement.namespaceURI===qt?n.createElement(t):n.createElementNS(e,t)}}function Bt(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function Yt(t){var n=It(t);return(n.local?Bt:Ot)(n)}function Lt(){}function jt(t){return null==t?Lt:function(){return this.querySelector(t)}}function Ht(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function Xt(){return[]}function Gt(t){return null==t?Xt:function(){return this.querySelectorAll(t)}}function Vt(t){return function(){return this.matches(t)}}function Wt(t){return function(n){return n.matches(t)}}var Zt=Array.prototype.find;function Kt(){return this.firstElementChild}var Qt=Array.prototype.filter;function Jt(){return Array.from(this.children)}function tn(t){return new Array(t.length)}function nn(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function en(t,n,e,r,i,o){for(var a,u=0,c=n.length,f=o.length;un?1:t>=n?0:NaN}function cn(t){return function(){this.removeAttribute(t)}}function fn(t){return function(){this.removeAttributeNS(t.space,t.local)}}function sn(t,n){return function(){this.setAttribute(t,n)}}function ln(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function hn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function dn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function pn(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function gn(t){return function(){this.style.removeProperty(t)}}function yn(t,n,e){return function(){this.style.setProperty(t,n,e)}}function vn(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function _n(t,n){return t.style.getPropertyValue(n)||pn(t).getComputedStyle(t,null).getPropertyValue(n)}function bn(t){return function(){delete this[t]}}function mn(t,n){return function(){this[t]=n}}function xn(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function wn(t){return t.trim().split(/^|\s+/)}function Mn(t){return t.classList||new Tn(t)}function Tn(t){this._node=t,this._names=wn(t.getAttribute("class")||"")}function An(t,n){for(var e=Mn(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Gn=[null];function Vn(t,n){this._groups=t,this._parents=n}function Wn(){return new Vn([[document.documentElement]],Gn)}function Zn(t){return"string"==typeof t?new Vn([[document.querySelector(t)]],[document.documentElement]):new Vn([[t]],Gn)}Vn.prototype=Wn.prototype={constructor:Vn,select:function(t){"function"!=typeof t&&(t=jt(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i=m&&(m=b+1);!(_=y[m])&&++m=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=un);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?gn:"function"==typeof n?vn:yn)(t,n,null==e?"":e)):_n(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?bn:"function"==typeof n?xn:mn)(t,n)):this.node()[t]},classed:function(t,n){var e=wn(t+"");if(arguments.length<2){for(var r=Mn(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?Ln:Yn,r=0;r()=>t;function fe(t,{sourceEvent:n,subject:e,target:r,identifier:i,active:o,x:a,y:u,dx:c,dy:f,dispatch:s}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},subject:{value:e,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:i,enumerable:!0,configurable:!0},active:{value:o,enumerable:!0,configurable:!0},x:{value:a,enumerable:!0,configurable:!0},y:{value:u,enumerable:!0,configurable:!0},dx:{value:c,enumerable:!0,configurable:!0},dy:{value:f,enumerable:!0,configurable:!0},_:{value:s}})}function se(t){return!t.ctrlKey&&!t.button}function le(){return this.parentNode}function he(t,n){return null==n?{x:t.x,y:t.y}:n}function de(){return navigator.maxTouchPoints||"ontouchstart"in this}function pe(t,n,e){t.prototype=n.prototype=e,e.constructor=t}function ge(t,n){var e=Object.create(t.prototype);for(var r in n)e[r]=n[r];return e}function ye(){}fe.prototype.on=function(){var t=this._.on.apply(this._,arguments);return t===this._?this:t};var ve=.7,_e=1/ve,be="\\s*([+-]?\\d+)\\s*",me="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",xe="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",we=/^#([0-9a-f]{3,8})$/,Me=new RegExp(`^rgb\\(${be},${be},${be}\\)$`),Te=new RegExp(`^rgb\\(${xe},${xe},${xe}\\)$`),Ae=new RegExp(`^rgba\\(${be},${be},${be},${me}\\)$`),Se=new RegExp(`^rgba\\(${xe},${xe},${xe},${me}\\)$`),Ee=new RegExp(`^hsl\\(${me},${xe},${xe}\\)$`),Ne=new RegExp(`^hsla\\(${me},${xe},${xe},${me}\\)$`),ke={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};function Ce(){return this.rgb().formatHex()}function Pe(){return this.rgb().formatRgb()}function ze(t){var n,e;return t=(t+"").trim().toLowerCase(),(n=we.exec(t))?(e=n[1].length,n=parseInt(n[1],16),6===e?$e(n):3===e?new qe(n>>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?De(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?De(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=Me.exec(t))?new qe(n[1],n[2],n[3],1):(n=Te.exec(t))?new qe(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Ae.exec(t))?De(n[1],n[2],n[3],n[4]):(n=Se.exec(t))?De(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Ee.exec(t))?Le(n[1],n[2]/100,n[3]/100,1):(n=Ne.exec(t))?Le(n[1],n[2]/100,n[3]/100,n[4]):ke.hasOwnProperty(t)?$e(ke[t]):"transparent"===t?new qe(NaN,NaN,NaN,0):null}function $e(t){return new qe(t>>16&255,t>>8&255,255&t,1)}function De(t,n,e,r){return r<=0&&(t=n=e=NaN),new qe(t,n,e,r)}function Re(t){return t instanceof ye||(t=ze(t)),t?new qe((t=t.rgb()).r,t.g,t.b,t.opacity):new qe}function Fe(t,n,e,r){return 1===arguments.length?Re(t):new qe(t,n,e,null==r?1:r)}function qe(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Ue(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}`}function Ie(){const t=Oe(this.opacity);return`${1===t?"rgb(":"rgba("}${Be(this.r)}, ${Be(this.g)}, ${Be(this.b)}${1===t?")":`, ${t})`}`}function Oe(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function Be(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function Ye(t){return((t=Be(t))<16?"0":"")+t.toString(16)}function Le(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Xe(t,n,e,r)}function je(t){if(t instanceof Xe)return new Xe(t.h,t.s,t.l,t.opacity);if(t instanceof ye||(t=ze(t)),!t)return new Xe;if(t instanceof Xe)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,c=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&c<1?0:a,new Xe(a,u,c,t.opacity)}function He(t,n,e,r){return 1===arguments.length?je(t):new Xe(t,n,e,null==r?1:r)}function Xe(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Ge(t){return(t=(t||0)%360)<0?t+360:t}function Ve(t){return Math.max(0,Math.min(1,t||0))}function We(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}pe(ye,ze,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:Ce,formatHex:Ce,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return je(this).formatHsl()},formatRgb:Pe,toString:Pe}),pe(qe,Fe,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new qe(Be(this.r),Be(this.g),Be(this.b),Oe(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Ue,formatHex:Ue,formatHex8:function(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}${Ye(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:Ie,toString:Ie})),pe(Xe,He,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new Xe(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new Xe(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new qe(We(t>=240?t-240:t+120,i,r),We(t,i,r),We(t<120?t+240:t-120,i,r),this.opacity)},clamp(){return new Xe(Ge(this.h),Ve(this.s),Ve(this.l),Oe(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=Oe(this.opacity);return`${1===t?"hsl(":"hsla("}${Ge(this.h)}, ${100*Ve(this.s)}%, ${100*Ve(this.l)}%${1===t?")":`, ${t})`}`}}));const Ze=Math.PI/180,Ke=180/Math.PI,Qe=.96422,Je=1,tr=.82521,nr=4/29,er=6/29,rr=3*er*er,ir=er*er*er;function or(t){if(t instanceof ur)return new ur(t.l,t.a,t.b,t.opacity);if(t instanceof pr)return gr(t);t instanceof qe||(t=Re(t));var n,e,r=lr(t.r),i=lr(t.g),o=lr(t.b),a=cr((.2225045*r+.7168786*i+.0606169*o)/Je);return r===i&&i===o?n=e=a:(n=cr((.4360747*r+.3850649*i+.1430804*o)/Qe),e=cr((.0139322*r+.0971045*i+.7141733*o)/tr)),new ur(116*a-16,500*(n-a),200*(a-e),t.opacity)}function ar(t,n,e,r){return 1===arguments.length?or(t):new ur(t,n,e,null==r?1:r)}function ur(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function cr(t){return t>ir?Math.pow(t,1/3):t/rr+nr}function fr(t){return t>er?t*t*t:rr*(t-nr)}function sr(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function lr(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function hr(t){if(t instanceof pr)return new pr(t.h,t.c,t.l,t.opacity);if(t instanceof ur||(t=or(t)),0===t.a&&0===t.b)return new pr(NaN,0=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r()=>t;function Cr(t,n){return function(e){return t+e*n}}function Pr(t,n){var e=n-t;return e?Cr(t,e>180||e<-180?e-360*Math.round(e/360):e):kr(isNaN(t)?n:t)}function zr(t){return 1==(t=+t)?$r:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):kr(isNaN(n)?e:n)}}function $r(t,n){var e=n-t;return e?Cr(t,e):kr(isNaN(t)?n:t)}var Dr=function t(n){var e=zr(n);function r(t,n){var r=e((t=Fe(t)).r,(n=Fe(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=$r(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function Rr(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,c.push({i:a,x:Yr(e,r)})),o=Hr.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:Yr(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,c),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:Yr(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,c),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:Yr(t,e)},{i:u-2,x:Yr(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,c),o=a=null,function(t){for(var n,e=-1,r=c.length;++e=0&&n._call.call(void 0,t),n=n._next;--yi}function Ci(){xi=(mi=Mi.now())+wi,yi=vi=0;try{ki()}finally{yi=0,function(){var t,n,e=pi,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:pi=n);gi=t,zi(r)}(),xi=0}}function Pi(){var t=Mi.now(),n=t-mi;n>bi&&(wi-=n,mi=t)}function zi(t){yi||(vi&&(vi=clearTimeout(vi)),t-xi>24?(t<1/0&&(vi=setTimeout(Ci,t-Mi.now()-wi)),_i&&(_i=clearInterval(_i))):(_i||(mi=Mi.now(),_i=setInterval(Pi,bi)),yi=1,Ti(Ci)))}function $i(t,n,e){var r=new Ei;return n=null==n?0:+n,r.restart((e=>{r.stop(),t(e+n)}),n,e),r}Ei.prototype=Ni.prototype={constructor:Ei,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?Ai():+e)+(null==n?0:+n),this._next||gi===this||(gi?gi._next=this:pi=this,gi=this),this._call=t,this._time=e,zi()},stop:function(){this._call&&(this._call=null,this._time=1/0,zi())}};var Di=$t("start","end","cancel","interrupt"),Ri=[],Fi=0,qi=1,Ui=2,Ii=3,Oi=4,Bi=5,Yi=6;function Li(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=qi,e.timer.restart(a,e.delay,e.time),e.delay<=t&&a(t-e.delay)}function a(o){var f,s,l,h;if(e.state!==qi)return c();for(f in i)if((h=i[f]).name===e.name){if(h.state===Ii)return $i(a);h.state===Oi?(h.state=Yi,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[f]):+fFi)throw new Error("too late; already scheduled");return e}function Hi(t,n){var e=Xi(t,n);if(e.state>Ii)throw new Error("too late; already running");return e}function Xi(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Gi(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>Ui&&e.state=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?ji:Hi;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=It(t),r="transform"===e?ni:Ki;return this.attrTween(t,"function"==typeof n?(e.local?ro:eo)(e,r,Zi(this,"attr."+t,n)):null==n?(e.local?Ji:Qi)(e):(e.local?no:to)(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=It(t);return this.tween(e,(r.local?io:oo)(r,n))},style:function(t,n,e){var r="transform"==(t+="")?ti:Ki;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=_n(this,t),a=(this.style.removeProperty(t),_n(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,lo(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=_n(this,t),u=e(this),c=u+"";return null==u&&(this.style.removeProperty(t),c=u=_n(this,t)),a===c?null:a===r&&c===i?o:(i=c,o=n(r=a,u))}}(t,r,Zi(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var c=Hi(this,t),f=c.on,s=null==c.value[a]?o||(o=lo(n)):void 0;f===e&&i===s||(r=(e=f).copy()).on(u,i=s),c.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=_n(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Zi(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var n="text";if(arguments.length<1)return(n=this.tween(n))&&n._value;if(null==t)return this.tween(n,null);if("function"!=typeof t)throw new Error;return this.tween(n,function(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&function(t){return function(n){this.textContent=t.call(this,n)}}(r)),n}return r._value=t,r}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}(this._id))},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Xi(this.node(),e).tween,o=0,a=i.length;o()=>t;function Qo(t,{sourceEvent:n,target:e,selection:r,mode:i,dispatch:o}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},selection:{value:r,enumerable:!0,configurable:!0},mode:{value:i,enumerable:!0,configurable:!0},_:{value:o}})}function Jo(t){t.preventDefault(),t.stopImmediatePropagation()}var ta={name:"drag"},na={name:"space"},ea={name:"handle"},ra={name:"center"};const{abs:ia,max:oa,min:aa}=Math;function ua(t){return[+t[0],+t[1]]}function ca(t){return[ua(t[0]),ua(t[1])]}var fa={name:"x",handles:["w","e"].map(va),input:function(t,n){return null==t?null:[[+t[0],n[0][1]],[+t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},sa={name:"y",handles:["n","s"].map(va),input:function(t,n){return null==t?null:[[n[0][0],+t[0]],[n[1][0],+t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},la={name:"xy",handles:["n","w","e","s","nw","ne","sw","se"].map(va),input:function(t){return null==t?null:ca(t)},output:function(t){return t}},ha={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},da={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},pa={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},ga={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},ya={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1};function va(t){return{type:t}}function _a(t){return!t.ctrlKey&&!t.button}function ba(){var t=this.ownerSVGElement||this;return t.hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]}function ma(){return navigator.maxTouchPoints||"ontouchstart"in this}function xa(t){for(;!t.__brush;)if(!(t=t.parentNode))return;return t.__brush}function wa(t){var n,e=ba,r=_a,i=ma,o=!0,a=$t("start","brush","end"),u=6;function c(n){var e=n.property("__brush",g).selectAll(".overlay").data([va("overlay")]);e.enter().append("rect").attr("class","overlay").attr("pointer-events","all").attr("cursor",ha.overlay).merge(e).each((function(){var t=xa(this).extent;Zn(this).attr("x",t[0][0]).attr("y",t[0][1]).attr("width",t[1][0]-t[0][0]).attr("height",t[1][1]-t[0][1])})),n.selectAll(".selection").data([va("selection")]).enter().append("rect").attr("class","selection").attr("cursor",ha.selection).attr("fill","#777").attr("fill-opacity",.3).attr("stroke","#fff").attr("shape-rendering","crispEdges");var r=n.selectAll(".handle").data(t.handles,(function(t){return t.type}));r.exit().remove(),r.enter().append("rect").attr("class",(function(t){return"handle handle--"+t.type})).attr("cursor",(function(t){return ha[t.type]})),n.each(f).attr("fill","none").attr("pointer-events","all").on("mousedown.brush",h).filter(i).on("touchstart.brush",h).on("touchmove.brush",d).on("touchend.brush touchcancel.brush",p).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function f(){var t=Zn(this),n=xa(this).selection;n?(t.selectAll(".selection").style("display",null).attr("x",n[0][0]).attr("y",n[0][1]).attr("width",n[1][0]-n[0][0]).attr("height",n[1][1]-n[0][1]),t.selectAll(".handle").style("display",null).attr("x",(function(t){return"e"===t.type[t.type.length-1]?n[1][0]-u/2:n[0][0]-u/2})).attr("y",(function(t){return"s"===t.type[0]?n[1][1]-u/2:n[0][1]-u/2})).attr("width",(function(t){return"n"===t.type||"s"===t.type?n[1][0]-n[0][0]+u:u})).attr("height",(function(t){return"e"===t.type||"w"===t.type?n[1][1]-n[0][1]+u:u}))):t.selectAll(".selection,.handle").style("display","none").attr("x",null).attr("y",null).attr("width",null).attr("height",null)}function s(t,n,e){var r=t.__brush.emitter;return!r||e&&r.clean?new l(t,n,e):r}function l(t,n,e){this.that=t,this.args=n,this.state=t.__brush,this.active=0,this.clean=e}function h(e){if((!n||e.touches)&&r.apply(this,arguments)){var i,a,u,c,l,h,d,p,g,y,v,_=this,b=e.target.__data__.type,m="selection"===(o&&e.metaKey?b="overlay":b)?ta:o&&e.altKey?ra:ea,x=t===sa?null:ga[b],w=t===fa?null:ya[b],M=xa(_),T=M.extent,A=M.selection,S=T[0][0],E=T[0][1],N=T[1][0],k=T[1][1],C=0,P=0,z=x&&w&&o&&e.shiftKey,$=Array.from(e.touches||[e],(t=>{const n=t.identifier;return(t=ne(t,_)).point0=t.slice(),t.identifier=n,t}));Gi(_);var D=s(_,arguments,!0).beforestart();if("overlay"===b){A&&(g=!0);const n=[$[0],$[1]||$[0]];M.selection=A=[[i=t===sa?S:aa(n[0][0],n[1][0]),u=t===fa?E:aa(n[0][1],n[1][1])],[l=t===sa?N:oa(n[0][0],n[1][0]),d=t===fa?k:oa(n[0][1],n[1][1])]],$.length>1&&I(e)}else i=A[0][0],u=A[0][1],l=A[1][0],d=A[1][1];a=i,c=u,h=l,p=d;var R=Zn(_).attr("pointer-events","none"),F=R.selectAll(".overlay").attr("cursor",ha[b]);if(e.touches)D.moved=U,D.ended=O;else{var q=Zn(e.view).on("mousemove.brush",U,!0).on("mouseup.brush",O,!0);o&&q.on("keydown.brush",(function(t){switch(t.keyCode){case 16:z=x&&w;break;case 18:m===ea&&(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra,I(t));break;case 32:m!==ea&&m!==ra||(x<0?l=h-C:x>0&&(i=a-C),w<0?d=p-P:w>0&&(u=c-P),m=na,F.attr("cursor",ha.selection),I(t));break;default:return}Jo(t)}),!0).on("keyup.brush",(function(t){switch(t.keyCode){case 16:z&&(y=v=z=!1,I(t));break;case 18:m===ra&&(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea,I(t));break;case 32:m===na&&(t.altKey?(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra):(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea),F.attr("cursor",ha[b]),I(t));break;default:return}Jo(t)}),!0),ae(e.view)}f.call(_),D.start(e,m.name)}function U(t){for(const n of t.changedTouches||[t])for(const t of $)t.identifier===n.identifier&&(t.cur=ne(n,_));if(z&&!y&&!v&&1===$.length){const t=$[0];ia(t.cur[0]-t[0])>ia(t.cur[1]-t[1])?v=!0:y=!0}for(const t of $)t.cur&&(t[0]=t.cur[0],t[1]=t.cur[1]);g=!0,Jo(t),I(t)}function I(t){const n=$[0],e=n.point0;var r;switch(C=n[0]-e[0],P=n[1]-e[1],m){case na:case ta:x&&(C=oa(S-i,aa(N-l,C)),a=i+C,h=l+C),w&&(P=oa(E-u,aa(k-d,P)),c=u+P,p=d+P);break;case ea:$[1]?(x&&(a=oa(S,aa(N,$[0][0])),h=oa(S,aa(N,$[1][0])),x=1),w&&(c=oa(E,aa(k,$[0][1])),p=oa(E,aa(k,$[1][1])),w=1)):(x<0?(C=oa(S-i,aa(N-i,C)),a=i+C,h=l):x>0&&(C=oa(S-l,aa(N-l,C)),a=i,h=l+C),w<0?(P=oa(E-u,aa(k-u,P)),c=u+P,p=d):w>0&&(P=oa(E-d,aa(k-d,P)),c=u,p=d+P));break;case ra:x&&(a=oa(S,aa(N,i-C*x)),h=oa(S,aa(N,l+C*x))),w&&(c=oa(E,aa(k,u-P*w)),p=oa(E,aa(k,d+P*w)))}ht+e))}function za(t,n){var e=0,r=null,i=null,o=null;function a(a){var u,c=a.length,f=new Array(c),s=Pa(0,c),l=new Array(c*c),h=new Array(c),d=0;a=Float64Array.from({length:c*c},n?(t,n)=>a[n%c][n/c|0]:(t,n)=>a[n/c|0][n%c]);for(let n=0;nr(f[t],f[n])));for(const e of s){const r=n;if(t){const t=Pa(1+~c,c).filter((t=>t<0?a[~t*c+e]:a[e*c+t]));i&&t.sort(((t,n)=>i(t<0?-a[~t*c+e]:a[e*c+t],n<0?-a[~n*c+e]:a[e*c+n])));for(const r of t)if(r<0){(l[~r*c+e]||(l[~r*c+e]={source:null,target:null})).target={index:e,startAngle:n,endAngle:n+=a[~r*c+e]*d,value:a[~r*c+e]}}else{(l[e*c+r]||(l[e*c+r]={source:null,target:null})).source={index:e,startAngle:n,endAngle:n+=a[e*c+r]*d,value:a[e*c+r]}}h[e]={index:e,startAngle:r,endAngle:n,value:f[e]}}else{const t=Pa(0,c).filter((t=>a[e*c+t]||a[t*c+e]));i&&t.sort(((t,n)=>i(a[e*c+t],a[e*c+n])));for(const r of t){let t;if(e=0))throw new Error(`invalid digits: ${t}`);if(n>15)return qa;const e=10**n;return function(t){this._+=t[0];for(let n=1,r=t.length;nRa)if(Math.abs(s*u-c*f)>Ra&&i){let h=e-o,d=r-a,p=u*u+c*c,g=h*h+d*d,y=Math.sqrt(p),v=Math.sqrt(l),_=i*Math.tan(($a-Math.acos((p+l-g)/(2*y*v)))/2),b=_/v,m=_/y;Math.abs(b-1)>Ra&&this._append`L${t+b*f},${n+b*s}`,this._append`A${i},${i},0,0,${+(s*h>f*d)},${this._x1=t+m*u},${this._y1=n+m*c}`}else this._append`L${this._x1=t},${this._y1=n}`;else;}arc(t,n,e,r,i,o){if(t=+t,n=+n,o=!!o,(e=+e)<0)throw new Error(`negative radius: ${e}`);let a=e*Math.cos(r),u=e*Math.sin(r),c=t+a,f=n+u,s=1^o,l=o?r-i:i-r;null===this._x1?this._append`M${c},${f}`:(Math.abs(this._x1-c)>Ra||Math.abs(this._y1-f)>Ra)&&this._append`L${c},${f}`,e&&(l<0&&(l=l%Da+Da),l>Fa?this._append`A${e},${e},0,1,${s},${t-a},${n-u}A${e},${e},0,1,${s},${this._x1=c},${this._y1=f}`:l>Ra&&this._append`A${e},${e},0,${+(l>=$a)},${s},${this._x1=t+e*Math.cos(i)},${this._y1=n+e*Math.sin(i)}`)}rect(t,n,e,r){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${e=+e}v${+r}h${-e}Z`}toString(){return this._}};function Ia(){return new Ua}Ia.prototype=Ua.prototype;var Oa=Array.prototype.slice;function Ba(t){return function(){return t}}function Ya(t){return t.source}function La(t){return t.target}function ja(t){return t.radius}function Ha(t){return t.startAngle}function Xa(t){return t.endAngle}function Ga(){return 0}function Va(){return 10}function Wa(t){var n=Ya,e=La,r=ja,i=ja,o=Ha,a=Xa,u=Ga,c=null;function f(){var f,s=n.apply(this,arguments),l=e.apply(this,arguments),h=u.apply(this,arguments)/2,d=Oa.call(arguments),p=+r.apply(this,(d[0]=s,d)),g=o.apply(this,d)-Ea,y=a.apply(this,d)-Ea,v=+i.apply(this,(d[0]=l,d)),_=o.apply(this,d)-Ea,b=a.apply(this,d)-Ea;if(c||(c=f=Ia()),h>Ca&&(Ma(y-g)>2*h+Ca?y>g?(g+=h,y-=h):(g-=h,y+=h):g=y=(g+y)/2,Ma(b-_)>2*h+Ca?b>_?(_+=h,b-=h):(_-=h,b+=h):_=b=(_+b)/2),c.moveTo(p*Ta(g),p*Aa(g)),c.arc(0,0,p,g,y),g!==_||y!==b)if(t){var m=v-+t.apply(this,arguments),x=(_+b)/2;c.quadraticCurveTo(0,0,m*Ta(_),m*Aa(_)),c.lineTo(v*Ta(x),v*Aa(x)),c.lineTo(m*Ta(b),m*Aa(b))}else c.quadraticCurveTo(0,0,v*Ta(_),v*Aa(_)),c.arc(0,0,v,_,b);if(c.quadraticCurveTo(0,0,p*Ta(g),p*Aa(g)),c.closePath(),f)return c=null,f+""||null}return t&&(f.headRadius=function(n){return arguments.length?(t="function"==typeof n?n:Ba(+n),f):t}),f.radius=function(t){return arguments.length?(r=i="function"==typeof t?t:Ba(+t),f):r},f.sourceRadius=function(t){return arguments.length?(r="function"==typeof t?t:Ba(+t),f):r},f.targetRadius=function(t){return arguments.length?(i="function"==typeof t?t:Ba(+t),f):i},f.startAngle=function(t){return arguments.length?(o="function"==typeof t?t:Ba(+t),f):o},f.endAngle=function(t){return arguments.length?(a="function"==typeof t?t:Ba(+t),f):a},f.padAngle=function(t){return arguments.length?(u="function"==typeof t?t:Ba(+t),f):u},f.source=function(t){return arguments.length?(n=t,f):n},f.target=function(t){return arguments.length?(e=t,f):e},f.context=function(t){return arguments.length?(c=null==t?null:t,f):c},f}var Za=Array.prototype.slice;function Ka(t,n){return t-n}var Qa=t=>()=>t;function Ja(t,n){for(var e,r=-1,i=n.length;++rr!=d>r&&e<(h-f)*(r-s)/(d-s)+f&&(i=-i)}return i}function nu(t,n,e){var r,i,o,a;return function(t,n,e){return(n[0]-t[0])*(e[1]-t[1])==(e[0]-t[0])*(n[1]-t[1])}(t,n,e)&&(i=t[r=+(t[0]===n[0])],o=e[r],a=n[r],i<=o&&o<=a||a<=o&&o<=i)}function eu(){}var ru=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function iu(){var t=1,n=1,e=K,r=u;function i(t){var n=e(t);if(Array.isArray(n))n=n.slice().sort(Ka);else{const e=M(t,ou);for(n=G(...Z(e[0],e[1],n),n);n[n.length-1]>=e[1];)n.pop();for(;n[1]o(t,n)))}function o(e,i){const o=null==i?NaN:+i;if(isNaN(o))throw new Error(`invalid value: ${i}`);var u=[],c=[];return function(e,r,i){var o,u,c,f,s,l,h=new Array,d=new Array;o=u=-1,f=au(e[0],r),ru[f<<1].forEach(p);for(;++o=r,ru[s<<2].forEach(p);for(;++o0?u.push([t]):c.push(t)})),c.forEach((function(t){for(var n,e=0,r=u.length;e0&&o0&&a=0&&o>=0))throw new Error("invalid size");return t=r,n=o,i},i.thresholds=function(t){return arguments.length?(e="function"==typeof t?t:Array.isArray(t)?Qa(Za.call(t)):Qa(t),i):e},i.smooth=function(t){return arguments.length?(r=t?u:eu,i):r===u},i}function ou(t){return isFinite(t)?t:NaN}function au(t,n){return null!=t&&+t>=n}function uu(t){return null==t||isNaN(t=+t)?-1/0:t}function cu(t,n,e,r){const i=r-n,o=e-n,a=isFinite(i)||isFinite(o)?i/o:Math.sign(i)/Math.sign(o);return isNaN(a)?t:t+a-.5}function fu(t){return t[0]}function su(t){return t[1]}function lu(){return 1}const hu=134217729,du=33306690738754706e-32;function pu(t,n,e,r,i){let o,a,u,c,f=n[0],s=r[0],l=0,h=0;s>f==s>-f?(o=f,f=n[++l]):(o=s,s=r[++h]);let d=0;if(lf==s>-f?(a=f+o,u=o-(a-f),f=n[++l]):(a=s+o,u=o-(a-s),s=r[++h]),o=a,0!==u&&(i[d++]=u);lf==s>-f?(a=o+f,c=a-o,u=o-(a-c)+(f-c),f=n[++l]):(a=o+s,c=a-o,u=o-(a-c)+(s-c),s=r[++h]),o=a,0!==u&&(i[d++]=u);for(;l=33306690738754716e-32*f?c:-function(t,n,e,r,i,o,a){let u,c,f,s,l,h,d,p,g,y,v,_,b,m,x,w,M,T;const A=t-i,S=e-i,E=n-o,N=r-o;m=A*N,h=hu*A,d=h-(h-A),p=A-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=E*S,h=hu*E,d=h-(h-E),p=E-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,_u[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,_u[1]=b-(v+l)+(l-w),T=_+v,l=T-_,_u[2]=_-(T-l)+(v-l),_u[3]=T;let k=function(t,n){let e=n[0];for(let r=1;r=C||-k>=C)return k;if(l=t-A,u=t-(A+l)+(l-i),l=e-S,f=e-(S+l)+(l-i),l=n-E,c=n-(E+l)+(l-o),l=r-N,s=r-(N+l)+(l-o),0===u&&0===c&&0===f&&0===s)return k;if(C=vu*a+du*Math.abs(k),k+=A*s+N*u-(E*f+S*c),k>=C||-k>=C)return k;m=u*N,h=hu*u,d=h-(h-u),p=u-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=c*S,h=hu*c,d=h-(h-c),p=c-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const P=pu(4,_u,4,wu,bu);m=A*s,h=hu*A,d=h-(h-A),p=A-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=E*f,h=hu*E,d=h-(h-E),p=E-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const z=pu(P,bu,4,wu,mu);m=u*s,h=hu*u,d=h-(h-u),p=u-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=c*f,h=hu*c,d=h-(h-c),p=c-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const $=pu(z,mu,4,wu,xu);return xu[$-1]}(t,n,e,r,i,o,f)}const Tu=Math.pow(2,-52),Au=new Uint32Array(512);class Su{static from(t,n=zu,e=$u){const r=t.length,i=new Float64Array(2*r);for(let o=0;o>1;if(n>0&&"number"!=typeof t[0])throw new Error("Expected coords to contain numbers.");this.coords=t;const e=Math.max(2*n-5,0);this._triangles=new Uint32Array(3*e),this._halfedges=new Int32Array(3*e),this._hashSize=Math.ceil(Math.sqrt(n)),this._hullPrev=new Uint32Array(n),this._hullNext=new Uint32Array(n),this._hullTri=new Uint32Array(n),this._hullHash=new Int32Array(this._hashSize),this._ids=new Uint32Array(n),this._dists=new Float64Array(n),this.update()}update(){const{coords:t,_hullPrev:n,_hullNext:e,_hullTri:r,_hullHash:i}=this,o=t.length>>1;let a=1/0,u=1/0,c=-1/0,f=-1/0;for(let n=0;nc&&(c=e),r>f&&(f=r),this._ids[n]=n}const s=(a+c)/2,l=(u+f)/2;let h,d,p;for(let n=0,e=1/0;n0&&(d=n,e=r)}let v=t[2*d],_=t[2*d+1],b=1/0;for(let n=0;nr&&(n[e++]=i,r=o)}return this.hull=n.subarray(0,e),this.triangles=new Uint32Array(0),void(this.halfedges=new Uint32Array(0))}if(Mu(g,y,v,_,m,x)<0){const t=d,n=v,e=_;d=p,v=m,_=x,p=t,m=n,x=e}const w=function(t,n,e,r,i,o){const a=e-t,u=r-n,c=i-t,f=o-n,s=a*a+u*u,l=c*c+f*f,h=.5/(a*f-u*c),d=t+(f*s-u*l)*h,p=n+(a*l-c*s)*h;return{x:d,y:p}}(g,y,v,_,m,x);this._cx=w.x,this._cy=w.y;for(let n=0;n0&&Math.abs(f-o)<=Tu&&Math.abs(s-a)<=Tu)continue;if(o=f,a=s,c===h||c===d||c===p)continue;let l=0;for(let t=0,n=this._hashKey(f,s);t=0;)if(y=g,y===l){y=-1;break}if(-1===y)continue;let v=this._addTriangle(y,c,e[y],-1,-1,r[y]);r[c]=this._legalize(v+2),r[y]=v,M++;let _=e[y];for(;g=e[_],Mu(f,s,t[2*_],t[2*_+1],t[2*g],t[2*g+1])<0;)v=this._addTriangle(_,c,g,r[c],-1,r[_]),r[c]=this._legalize(v+2),e[_]=_,M--,_=g;if(y===l)for(;g=n[y],Mu(f,s,t[2*g],t[2*g+1],t[2*y],t[2*y+1])<0;)v=this._addTriangle(g,c,y,-1,r[y],r[g]),this._legalize(v+2),r[g]=v,e[y]=y,M--,y=g;this._hullStart=n[c]=y,e[y]=n[_]=c,e[c]=_,i[this._hashKey(f,s)]=c,i[this._hashKey(t[2*y],t[2*y+1])]=y}this.hull=new Uint32Array(M);for(let t=0,n=this._hullStart;t0?3-e:1+e)/4}(t-this._cx,n-this._cy)*this._hashSize)%this._hashSize}_legalize(t){const{_triangles:n,_halfedges:e,coords:r}=this;let i=0,o=0;for(;;){const a=e[t],u=t-t%3;if(o=u+(t+2)%3,-1===a){if(0===i)break;t=Au[--i];continue}const c=a-a%3,f=u+(t+1)%3,s=c+(a+2)%3,l=n[o],h=n[t],d=n[f],p=n[s];if(Nu(r[2*l],r[2*l+1],r[2*h],r[2*h+1],r[2*d],r[2*d+1],r[2*p],r[2*p+1])){n[t]=p,n[a]=l;const r=e[s];if(-1===r){let n=this._hullStart;do{if(this._hullTri[n]===s){this._hullTri[n]=t;break}n=this._hullPrev[n]}while(n!==this._hullStart)}this._link(t,r),this._link(a,e[o]),this._link(o,s);const u=c+(a+1)%3;i=e&&n[t[a]]>o;)t[a+1]=t[a--];t[a+1]=r}else{let i=e+1,o=r;Pu(t,e+r>>1,i),n[t[e]]>n[t[r]]&&Pu(t,e,r),n[t[i]]>n[t[r]]&&Pu(t,i,r),n[t[e]]>n[t[i]]&&Pu(t,e,i);const a=t[i],u=n[a];for(;;){do{i++}while(n[t[i]]u);if(o=o-e?(Cu(t,n,i,r),Cu(t,n,e,o-1)):(Cu(t,n,e,o-1),Cu(t,n,i,r))}}function Pu(t,n,e){const r=t[n];t[n]=t[e],t[e]=r}function zu(t){return t[0]}function $u(t){return t[1]}const Du=1e-6;class Ru{constructor(){this._x0=this._y0=this._x1=this._y1=null,this._=""}moveTo(t,n){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}`}closePath(){null!==this._x1&&(this._x1=this._x0,this._y1=this._y0,this._+="Z")}lineTo(t,n){this._+=`L${this._x1=+t},${this._y1=+n}`}arc(t,n,e){const r=(t=+t)+(e=+e),i=n=+n;if(e<0)throw new Error("negative radius");null===this._x1?this._+=`M${r},${i}`:(Math.abs(this._x1-r)>Du||Math.abs(this._y1-i)>Du)&&(this._+="L"+r+","+i),e&&(this._+=`A${e},${e},0,1,1,${t-e},${n}A${e},${e},0,1,1,${this._x1=r},${this._y1=i}`)}rect(t,n,e,r){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${+e}v${+r}h${-e}Z`}value(){return this._||null}}class Fu{constructor(){this._=[]}moveTo(t,n){this._.push([t,n])}closePath(){this._.push(this._[0].slice())}lineTo(t,n){this._.push([t,n])}value(){return this._.length?this._:null}}class qu{constructor(t,[n,e,r,i]=[0,0,960,500]){if(!((r=+r)>=(n=+n)&&(i=+i)>=(e=+e)))throw new Error("invalid bounds");this.delaunay=t,this._circumcenters=new Float64Array(2*t.points.length),this.vectors=new Float64Array(2*t.points.length),this.xmax=r,this.xmin=n,this.ymax=i,this.ymin=e,this._init()}update(){return this.delaunay.update(),this._init(),this}_init(){const{delaunay:{points:t,hull:n,triangles:e},vectors:r}=this;let i,o;const a=this.circumcenters=this._circumcenters.subarray(0,e.length/3*2);for(let r,u,c=0,f=0,s=e.length;c1;)i-=2;for(let t=2;t0){if(n>=this.ymax)return null;(i=(this.ymax-n)/r)0){if(t>=this.xmax)return null;(i=(this.xmax-t)/e)this.xmax?2:0)|(nthis.ymax?8:0)}_simplify(t){if(t&&t.length>4){for(let n=0;n2&&function(t){const{triangles:n,coords:e}=t;for(let t=0;t1e-10)return!1}return!0}(t)){this.collinear=Int32Array.from({length:n.length/2},((t,n)=>n)).sort(((t,e)=>n[2*t]-n[2*e]||n[2*t+1]-n[2*e+1]));const t=this.collinear[0],e=this.collinear[this.collinear.length-1],r=[n[2*t],n[2*t+1],n[2*e],n[2*e+1]],i=1e-8*Math.hypot(r[3]-r[1],r[2]-r[0]);for(let t=0,e=n.length/2;t0&&(this.triangles=new Int32Array(3).fill(-1),this.halfedges=new Int32Array(3).fill(-1),this.triangles[0]=r[0],o[r[0]]=1,2===r.length&&(o[r[1]]=0,this.triangles[1]=r[1],this.triangles[2]=r[1]))}voronoi(t){return new qu(this,t)}*neighbors(t){const{inedges:n,hull:e,_hullIndex:r,halfedges:i,triangles:o,collinear:a}=this;if(a){const n=a.indexOf(t);return n>0&&(yield a[n-1]),void(n=0&&i!==e&&i!==r;)e=i;return i}_step(t,n,e){const{inedges:r,hull:i,_hullIndex:o,halfedges:a,triangles:u,points:c}=this;if(-1===r[t]||!c.length)return(t+1)%(c.length>>1);let f=t,s=Iu(n-c[2*t],2)+Iu(e-c[2*t+1],2);const l=r[t];let h=l;do{let r=u[h];const l=Iu(n-c[2*r],2)+Iu(e-c[2*r+1],2);if(l9999?"+"+Ku(n,6):Ku(n,4))+"-"+Ku(t.getUTCMonth()+1,2)+"-"+Ku(t.getUTCDate(),2)+(o?"T"+Ku(e,2)+":"+Ku(r,2)+":"+Ku(i,2)+"."+Ku(o,3)+"Z":i?"T"+Ku(e,2)+":"+Ku(r,2)+":"+Ku(i,2)+"Z":r||e?"T"+Ku(e,2)+":"+Ku(r,2)+"Z":"")}function Ju(t){var n=new RegExp('["'+t+"\n\r]"),e=t.charCodeAt(0);function r(t,n){var r,i=[],o=t.length,a=0,u=0,c=o<=0,f=!1;function s(){if(c)return Hu;if(f)return f=!1,ju;var n,r,i=a;if(t.charCodeAt(i)===Xu){for(;a++=o?c=!0:(r=t.charCodeAt(a++))===Gu?f=!0:r===Vu&&(f=!0,t.charCodeAt(a)===Gu&&++a),t.slice(i+1,n-1).replace(/""/g,'"')}for(;amc(n,e).then((n=>(new DOMParser).parseFromString(n,t)))}var Sc=Ac("application/xml"),Ec=Ac("text/html"),Nc=Ac("image/svg+xml");function kc(t,n,e,r){if(isNaN(n)||isNaN(e))return t;var i,o,a,u,c,f,s,l,h,d=t._root,p={data:r},g=t._x0,y=t._y0,v=t._x1,_=t._y1;if(!d)return t._root=p,t;for(;d.length;)if((f=n>=(o=(g+v)/2))?g=o:v=o,(s=e>=(a=(y+_)/2))?y=a:_=a,i=d,!(d=d[l=s<<1|f]))return i[l]=p,t;if(u=+t._x.call(null,d.data),c=+t._y.call(null,d.data),n===u&&e===c)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(f=n>=(o=(g+v)/2))?g=o:v=o,(s=e>=(a=(y+_)/2))?y=a:_=a}while((l=s<<1|f)==(h=(c>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function Cc(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function Pc(t){return t[0]}function zc(t){return t[1]}function $c(t,n,e){var r=new Dc(null==n?Pc:n,null==e?zc:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function Dc(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function Rc(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}var Fc=$c.prototype=Dc.prototype;function qc(t){return function(){return t}}function Uc(t){return 1e-6*(t()-.5)}function Ic(t){return t.x+t.vx}function Oc(t){return t.y+t.vy}function Bc(t){return t.index}function Yc(t,n){var e=t.get(n);if(!e)throw new Error("node not found: "+n);return e}Fc.copy=function(){var t,n,e=new Dc(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=Rc(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=Rc(n));return e},Fc.add=function(t){const n=+this._x.call(null,t),e=+this._y.call(null,t);return kc(this.cover(n,e),n,e,t)},Fc.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),c=1/0,f=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));if(c>s||f>l)return this;for(this.cover(c,f).cover(s,l),e=0;et||t>=i||r>n||n>=o;)switch(u=(nh||(o=c.y0)>d||(a=c.x1)=v)<<1|t>=y)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-f],p[p.length-1-f]=c)}else{var _=t-+this._x.call(null,g.data),b=n-+this._y.call(null,g.data),m=_*_+b*b;if(m=(u=(p+y)/2))?p=u:y=u,(s=a>=(c=(g+v)/2))?g=c:v=c,n=d,!(d=d[l=s<<1|f]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},Fc.removeAll=function(t){for(var n=0,e=t.length;n1?r[0]+r.slice(2):r,+t.slice(e+1)]}function Zc(t){return(t=Wc(Math.abs(t)))?t[1]:NaN}var Kc,Qc=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Jc(t){if(!(n=Qc.exec(t)))throw new Error("invalid format: "+t);var n;return new tf({fill:n[1],align:n[2],sign:n[3],symbol:n[4],zero:n[5],width:n[6],comma:n[7],precision:n[8]&&n[8].slice(1),trim:n[9],type:n[10]})}function tf(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function nf(t,n){var e=Wc(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}Jc.prototype=tf.prototype,tf.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var ef={"%":(t,n)=>(100*t).toFixed(n),b:t=>Math.round(t).toString(2),c:t=>t+"",d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:(t,n)=>t.toExponential(n),f:(t,n)=>t.toFixed(n),g:(t,n)=>t.toPrecision(n),o:t=>Math.round(t).toString(8),p:(t,n)=>nf(100*t,n),r:nf,s:function(t,n){var e=Wc(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(Kc=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+Wc(t,Math.max(0,n+o-1))[0]},X:t=>Math.round(t).toString(16).toUpperCase(),x:t=>Math.round(t).toString(16)};function rf(t){return t}var of,af=Array.prototype.map,uf=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function cf(t){var n,e,r=void 0===t.grouping||void 0===t.thousands?rf:(n=af.call(t.grouping,Number),e=t.thousands+"",function(t,r){for(var i=t.length,o=[],a=0,u=n[0],c=0;i>0&&u>0&&(c+u+1>r&&(u=Math.max(1,r-c)),o.push(t.substring(i-=u,i+u)),!((c+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}),i=void 0===t.currency?"":t.currency[0]+"",o=void 0===t.currency?"":t.currency[1]+"",a=void 0===t.decimal?".":t.decimal+"",u=void 0===t.numerals?rf:function(t){return function(n){return n.replace(/[0-9]/g,(function(n){return t[+n]}))}}(af.call(t.numerals,String)),c=void 0===t.percent?"%":t.percent+"",f=void 0===t.minus?"−":t.minus+"",s=void 0===t.nan?"NaN":t.nan+"";function l(t){var n=(t=Jc(t)).fill,e=t.align,l=t.sign,h=t.symbol,d=t.zero,p=t.width,g=t.comma,y=t.precision,v=t.trim,_=t.type;"n"===_?(g=!0,_="g"):ef[_]||(void 0===y&&(y=12),v=!0,_="g"),(d||"0"===n&&"="===e)&&(d=!0,n="0",e="=");var b="$"===h?i:"#"===h&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",m="$"===h?o:/[%p]/.test(_)?c:"",x=ef[_],w=/[defgprs%]/.test(_);function M(t){var i,o,c,h=b,M=m;if("c"===_)M=x(t)+M,t="";else{var T=(t=+t)<0||1/t<0;if(t=isNaN(t)?s:x(Math.abs(t),y),v&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),T&&0==+t&&"+"!==l&&(T=!1),h=(T?"("===l?l:f:"-"===l||"("===l?"":l)+h,M=("s"===_?uf[8+Kc/3]:"")+M+(T&&"("===l?")":""),w)for(i=-1,o=t.length;++i(c=t.charCodeAt(i))||c>57){M=(46===c?a+t.slice(i+1):t.slice(i))+M,t=t.slice(0,i);break}}g&&!d&&(t=r(t,1/0));var A=h.length+t.length+M.length,S=A>1)+h+t+M+S.slice(A);break;default:t=S+h+t+M}return u(t)}return y=void 0===y?6:/[gprs]/.test(_)?Math.max(1,Math.min(21,y)):Math.max(0,Math.min(20,y)),M.toString=function(){return t+""},M}return{format:l,formatPrefix:function(t,n){var e=l(((t=Jc(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Zc(n)/3))),i=Math.pow(10,-r),o=uf[8+r/3];return function(t){return e(i*t)+o}}}}function ff(n){return of=cf(n),t.format=of.format,t.formatPrefix=of.formatPrefix,of}function sf(t){return Math.max(0,-Zc(Math.abs(t)))}function lf(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Zc(n)/3)))-Zc(Math.abs(t)))}function hf(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Zc(n)-Zc(t))+1}t.format=void 0,t.formatPrefix=void 0,ff({thousands:",",grouping:[3],currency:["$",""]});var df=1e-6,pf=1e-12,gf=Math.PI,yf=gf/2,vf=gf/4,_f=2*gf,bf=180/gf,mf=gf/180,xf=Math.abs,wf=Math.atan,Mf=Math.atan2,Tf=Math.cos,Af=Math.ceil,Sf=Math.exp,Ef=Math.hypot,Nf=Math.log,kf=Math.pow,Cf=Math.sin,Pf=Math.sign||function(t){return t>0?1:t<0?-1:0},zf=Math.sqrt,$f=Math.tan;function Df(t){return t>1?0:t<-1?gf:Math.acos(t)}function Rf(t){return t>1?yf:t<-1?-yf:Math.asin(t)}function Ff(t){return(t=Cf(t/2))*t}function qf(){}function Uf(t,n){t&&Of.hasOwnProperty(t.type)&&Of[t.type](t,n)}var If={Feature:function(t,n){Uf(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r=0?1:-1,i=r*e,o=Tf(n=(n*=mf)/2+vf),a=Cf(n),u=Vf*a,c=Gf*o+u*Tf(i),f=u*r*Cf(i);as.add(Mf(f,c)),Xf=t,Gf=o,Vf=a}function ds(t){return[Mf(t[1],t[0]),Rf(t[2])]}function ps(t){var n=t[0],e=t[1],r=Tf(e);return[r*Tf(n),r*Cf(n),Cf(e)]}function gs(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function ys(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function vs(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function _s(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function bs(t){var n=zf(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}var ms,xs,ws,Ms,Ts,As,Ss,Es,Ns,ks,Cs,Ps,zs,$s,Ds,Rs,Fs={point:qs,lineStart:Is,lineEnd:Os,polygonStart:function(){Fs.point=Bs,Fs.lineStart=Ys,Fs.lineEnd=Ls,rs=new T,cs.polygonStart()},polygonEnd:function(){cs.polygonEnd(),Fs.point=qs,Fs.lineStart=Is,Fs.lineEnd=Os,as<0?(Wf=-(Kf=180),Zf=-(Qf=90)):rs>df?Qf=90:rs<-df&&(Zf=-90),os[0]=Wf,os[1]=Kf},sphere:function(){Wf=-(Kf=180),Zf=-(Qf=90)}};function qs(t,n){is.push(os=[Wf=t,Kf=t]),nQf&&(Qf=n)}function Us(t,n){var e=ps([t*mf,n*mf]);if(es){var r=ys(es,e),i=ys([r[1],-r[0],0],r);bs(i),i=ds(i);var o,a=t-Jf,u=a>0?1:-1,c=i[0]*bf*u,f=xf(a)>180;f^(u*JfQf&&(Qf=o):f^(u*Jf<(c=(c+360)%360-180)&&cQf&&(Qf=n)),f?tjs(Wf,Kf)&&(Kf=t):js(t,Kf)>js(Wf,Kf)&&(Wf=t):Kf>=Wf?(tKf&&(Kf=t)):t>Jf?js(Wf,t)>js(Wf,Kf)&&(Kf=t):js(t,Kf)>js(Wf,Kf)&&(Wf=t)}else is.push(os=[Wf=t,Kf=t]);nQf&&(Qf=n),es=e,Jf=t}function Is(){Fs.point=Us}function Os(){os[0]=Wf,os[1]=Kf,Fs.point=qs,es=null}function Bs(t,n){if(es){var e=t-Jf;rs.add(xf(e)>180?e+(e>0?360:-360):e)}else ts=t,ns=n;cs.point(t,n),Us(t,n)}function Ys(){cs.lineStart()}function Ls(){Bs(ts,ns),cs.lineEnd(),xf(rs)>df&&(Wf=-(Kf=180)),os[0]=Wf,os[1]=Kf,es=null}function js(t,n){return(n-=t)<0?n+360:n}function Hs(t,n){return t[0]-n[0]}function Xs(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:ngf&&(t-=Math.round(t/_f)*_f),[t,n]}function ul(t,n,e){return(t%=_f)?n||e?ol(fl(t),sl(n,e)):fl(t):n||e?sl(n,e):al}function cl(t){return function(n,e){return xf(n+=t)>gf&&(n-=Math.round(n/_f)*_f),[n,e]}}function fl(t){var n=cl(t);return n.invert=cl(-t),n}function sl(t,n){var e=Tf(t),r=Cf(t),i=Tf(n),o=Cf(n);function a(t,n){var a=Tf(n),u=Tf(t)*a,c=Cf(t)*a,f=Cf(n),s=f*e+u*r;return[Mf(c*i-s*o,u*e-f*r),Rf(s*i+c*o)]}return a.invert=function(t,n){var a=Tf(n),u=Tf(t)*a,c=Cf(t)*a,f=Cf(n),s=f*i-c*o;return[Mf(c*i+f*o,u*e+s*r),Rf(s*e-u*r)]},a}function ll(t){function n(n){return(n=t(n[0]*mf,n[1]*mf))[0]*=bf,n[1]*=bf,n}return t=ul(t[0]*mf,t[1]*mf,t.length>2?t[2]*mf:0),n.invert=function(n){return(n=t.invert(n[0]*mf,n[1]*mf))[0]*=bf,n[1]*=bf,n},n}function hl(t,n,e,r,i,o){if(e){var a=Tf(n),u=Cf(n),c=r*e;null==i?(i=n+r*_f,o=n-c/2):(i=dl(a,i),o=dl(a,o),(r>0?io)&&(i+=r*_f));for(var f,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function gl(t,n){return xf(t[0]-n[0])=0;--o)i.point((s=f[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}f=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function _l(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,E=S*A,N=E>gf,k=y*w;if(c.add(Mf(k*S*Cf(E),v*M+k*Tf(E))),a+=N?A+S*_f:A,N^p>=e^m>=e){var C=ys(ps(d),ps(b));bs(C);var P=ys(o,C);bs(P);var z=(N^A>=0?-1:1)*Rf(P[2]);(r>z||r===z&&(C[0]||C[1]))&&(u+=N^A>=0?1:-1)}}return(a<-df||a0){for(l||(i.polygonStart(),l=!0),i.lineStart(),t=0;t1&&2&c&&h.push(h.pop().concat(h.shift())),a.push(h.filter(wl))}return h}}function wl(t){return t.length>1}function Ml(t,n){return((t=t.x)[0]<0?t[1]-yf-df:yf-t[1])-((n=n.x)[0]<0?n[1]-yf-df:yf-n[1])}al.invert=al;var Tl=xl((function(){return!0}),(function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?gf:-gf,c=xf(o-e);xf(c-gf)0?yf:-yf),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&c>=gf&&(xf(e-i)df?wf((Cf(n)*(o=Tf(r))*Cf(e)-Cf(r)*(i=Tf(n))*Cf(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}}),(function(t,n,e,r){var i;if(null==t)i=e*yf,r.point(-gf,i),r.point(0,i),r.point(gf,i),r.point(gf,0),r.point(gf,-i),r.point(0,-i),r.point(-gf,-i),r.point(-gf,0),r.point(-gf,i);else if(xf(t[0]-n[0])>df){var o=t[0]0,i=xf(n)>df;function o(t,e){return Tf(t)*Tf(e)>n}function a(t,e,r){var i=[1,0,0],o=ys(ps(t),ps(e)),a=gs(o,o),u=o[0],c=a-u*u;if(!c)return!r&&t;var f=n*a/c,s=-n*u/c,l=ys(i,o),h=_s(i,f);vs(h,_s(o,s));var d=l,p=gs(h,d),g=gs(d,d),y=p*p-g*(gs(h,h)-1);if(!(y<0)){var v=zf(y),_=_s(d,(-p-v)/g);if(vs(_,h),_=ds(_),!r)return _;var b,m=t[0],x=e[0],w=t[1],M=e[1];x0^_[1]<(xf(_[0]-m)gf^(m<=_[0]&&_[0]<=x)){var S=_s(d,(-p+v)/g);return vs(S,h),[_,ds(S)]}}}function u(n,e){var i=r?t:gf-t,o=0;return n<-i?o|=1:n>i&&(o|=2),e<-i?o|=4:e>i&&(o|=8),o}return xl(o,(function(t){var n,e,c,f,s;return{lineStart:function(){f=c=!1,s=1},point:function(l,h){var d,p=[l,h],g=o(l,h),y=r?g?0:u(l,h):g?u(l+(l<0?gf:-gf),h):0;if(!n&&(f=c=g)&&t.lineStart(),g!==c&&(!(d=a(n,p))||gl(n,d)||gl(p,d))&&(p[2]=1),g!==c)s=0,g?(t.lineStart(),d=a(p,n),t.point(d[0],d[1])):(d=a(n,p),t.point(d[0],d[1],2),t.lineEnd()),n=d;else if(i&&n&&r^g){var v;y&e||!(v=a(p,n,!0))||(s=0,r?(t.lineStart(),t.point(v[0][0],v[0][1]),t.point(v[1][0],v[1][1]),t.lineEnd()):(t.point(v[1][0],v[1][1]),t.lineEnd(),t.lineStart(),t.point(v[0][0],v[0][1],3)))}!g||n&&gl(n,p)||t.point(p[0],p[1]),n=p,c=g,e=y},lineEnd:function(){c&&t.lineEnd(),n=null},clean:function(){return s|(f&&c)<<1}}}),(function(n,r,i,o){hl(o,t,e,i,n,r)}),r?[0,-t]:[-gf,t-gf])}var Sl,El,Nl,kl,Cl=1e9,Pl=-Cl;function zl(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,f){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||c(i,o)<0^u>0)do{f.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else f.point(o[0],o[1])}function a(r,i){return xf(r[0]-t)0?0:3:xf(r[0]-e)0?2:1:xf(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return c(t.x,n.x)}function c(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){var c,f,s,l,h,d,p,g,y,v,_,b=a,m=pl(),x={point:w,lineStart:function(){x.point=M,f&&f.push(s=[]);v=!0,y=!1,p=g=NaN},lineEnd:function(){c&&(M(l,h),d&&y&&m.rejoin(),c.push(m.result()));x.point=w,y&&b.lineEnd()},polygonStart:function(){b=m,c=[],f=[],_=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=f.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=_&&n,i=(c=ft(c)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&vl(c,u,n,o,a),a.polygonEnd());b=a,c=f=s=null}};function w(t,n){i(t,n)&&b.point(t,n)}function M(o,a){var u=i(o,a);if(f&&s.push([o,a]),v)l=o,h=a,d=u,v=!1,u&&(b.lineStart(),b.point(o,a));else if(u&&y)b.point(o,a);else{var c=[p=Math.max(Pl,Math.min(Cl,p)),g=Math.max(Pl,Math.min(Cl,g))],m=[o=Math.max(Pl,Math.min(Cl,o)),a=Math.max(Pl,Math.min(Cl,a))];!function(t,n,e,r,i,o){var a,u=t[0],c=t[1],f=0,s=1,l=n[0]-u,h=n[1]-c;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>f&&(f=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=o-c,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>f&&(f=a)}else if(h>0){if(a0&&(t[0]=u+f*l,t[1]=c+f*h),s<1&&(n[0]=u+s*l,n[1]=c+s*h),!0}}}}}(c,m,t,n,e,r)?u&&(b.lineStart(),b.point(o,a),_=!1):(y||(b.lineStart(),b.point(c[0],c[1])),b.point(m[0],m[1]),u||b.lineEnd(),_=!1)}p=o,g=a,y=u}return x}}var $l={sphere:qf,point:qf,lineStart:function(){$l.point=Rl,$l.lineEnd=Dl},lineEnd:qf,polygonStart:qf,polygonEnd:qf};function Dl(){$l.point=$l.lineEnd=qf}function Rl(t,n){El=t*=mf,Nl=Cf(n*=mf),kl=Tf(n),$l.point=Fl}function Fl(t,n){t*=mf;var e=Cf(n*=mf),r=Tf(n),i=xf(t-El),o=Tf(i),a=r*Cf(i),u=kl*e-Nl*r*o,c=Nl*e+kl*r*o;Sl.add(Mf(zf(a*a+u*u),c)),El=t,Nl=e,kl=r}function ql(t){return Sl=new T,Lf(t,$l),+Sl}var Ul=[null,null],Il={type:"LineString",coordinates:Ul};function Ol(t,n){return Ul[0]=t,Ul[1]=n,ql(Il)}var Bl={Feature:function(t,n){return Ll(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r0&&(i=Ol(t[o],t[o-1]))>0&&e<=i&&r<=i&&(e+r-i)*(1-Math.pow((e-r)/i,2))df})).map(c)).concat(lt(Af(o/d)*d,i,d).filter((function(t){return xf(t%g)>df})).map(f))}return v.lines=function(){return _().map((function(t){return{type:"LineString",coordinates:t}}))},v.outline=function(){return{type:"Polygon",coordinates:[s(r).concat(l(a).slice(1),s(e).reverse().slice(1),l(u).reverse().slice(1))]}},v.extent=function(t){return arguments.length?v.extentMajor(t).extentMinor(t):v.extentMinor()},v.extentMajor=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],u=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),u>a&&(t=u,u=a,a=t),v.precision(y)):[[r,u],[e,a]]},v.extentMinor=function(e){return arguments.length?(n=+e[0][0],t=+e[1][0],o=+e[0][1],i=+e[1][1],n>t&&(e=n,n=t,t=e),o>i&&(e=o,o=i,i=e),v.precision(y)):[[n,o],[t,i]]},v.step=function(t){return arguments.length?v.stepMajor(t).stepMinor(t):v.stepMinor()},v.stepMajor=function(t){return arguments.length?(p=+t[0],g=+t[1],v):[p,g]},v.stepMinor=function(t){return arguments.length?(h=+t[0],d=+t[1],v):[h,d]},v.precision=function(h){return arguments.length?(y=+h,c=Wl(o,i,90),f=Zl(n,t,y),s=Wl(u,a,90),l=Zl(r,e,y),v):y},v.extentMajor([[-180,-90+df],[180,90-df]]).extentMinor([[-180,-80-df],[180,80+df]])}var Ql,Jl,th,nh,eh=t=>t,rh=new T,ih=new T,oh={point:qf,lineStart:qf,lineEnd:qf,polygonStart:function(){oh.lineStart=ah,oh.lineEnd=fh},polygonEnd:function(){oh.lineStart=oh.lineEnd=oh.point=qf,rh.add(xf(ih)),ih=new T},result:function(){var t=rh/2;return rh=new T,t}};function ah(){oh.point=uh}function uh(t,n){oh.point=ch,Ql=th=t,Jl=nh=n}function ch(t,n){ih.add(nh*t-th*n),th=t,nh=n}function fh(){ch(Ql,Jl)}var sh=oh,lh=1/0,hh=lh,dh=-lh,ph=dh,gh={point:function(t,n){tdh&&(dh=t);nph&&(ph=n)},lineStart:qf,lineEnd:qf,polygonStart:qf,polygonEnd:qf,result:function(){var t=[[lh,hh],[dh,ph]];return dh=ph=-(hh=lh=1/0),t}};var yh,vh,_h,bh,mh=gh,xh=0,wh=0,Mh=0,Th=0,Ah=0,Sh=0,Eh=0,Nh=0,kh=0,Ch={point:Ph,lineStart:zh,lineEnd:Rh,polygonStart:function(){Ch.lineStart=Fh,Ch.lineEnd=qh},polygonEnd:function(){Ch.point=Ph,Ch.lineStart=zh,Ch.lineEnd=Rh},result:function(){var t=kh?[Eh/kh,Nh/kh]:Sh?[Th/Sh,Ah/Sh]:Mh?[xh/Mh,wh/Mh]:[NaN,NaN];return xh=wh=Mh=Th=Ah=Sh=Eh=Nh=kh=0,t}};function Ph(t,n){xh+=t,wh+=n,++Mh}function zh(){Ch.point=$h}function $h(t,n){Ch.point=Dh,Ph(_h=t,bh=n)}function Dh(t,n){var e=t-_h,r=n-bh,i=zf(e*e+r*r);Th+=i*(_h+t)/2,Ah+=i*(bh+n)/2,Sh+=i,Ph(_h=t,bh=n)}function Rh(){Ch.point=Ph}function Fh(){Ch.point=Uh}function qh(){Ih(yh,vh)}function Uh(t,n){Ch.point=Ih,Ph(yh=_h=t,vh=bh=n)}function Ih(t,n){var e=t-_h,r=n-bh,i=zf(e*e+r*r);Th+=i*(_h+t)/2,Ah+=i*(bh+n)/2,Sh+=i,Eh+=(i=bh*t-_h*n)*(_h+t),Nh+=i*(bh+n),kh+=3*i,Ph(_h=t,bh=n)}var Oh=Ch;function Bh(t){this._context=t}Bh.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,_f)}},result:qf};var Yh,Lh,jh,Hh,Xh,Gh=new T,Vh={point:qf,lineStart:function(){Vh.point=Wh},lineEnd:function(){Yh&&Zh(Lh,jh),Vh.point=qf},polygonStart:function(){Yh=!0},polygonEnd:function(){Yh=null},result:function(){var t=+Gh;return Gh=new T,t}};function Wh(t,n){Vh.point=Zh,Lh=Hh=t,jh=Xh=n}function Zh(t,n){Hh-=t,Xh-=n,Gh.add(zf(Hh*Hh+Xh*Xh)),Hh=t,Xh=n}var Kh=Vh;let Qh,Jh,td,nd;class ed{constructor(t){this._append=null==t?rd:function(t){const n=Math.floor(t);if(!(n>=0))throw new RangeError(`invalid digits: ${t}`);if(n>15)return rd;if(n!==Qh){const t=10**n;Qh=n,Jh=function(n){let e=1;this._+=n[0];for(const r=n.length;e4*n&&g--){var m=a+h,x=u+d,w=c+p,M=zf(m*m+x*x+w*w),T=Rf(w/=M),A=xf(xf(w)-1)n||xf((v*k+_*C)/b-.5)>.3||a*h+u*d+c*p2?t[2]%360*mf:0,k()):[y*bf,v*bf,_*bf]},E.angle=function(t){return arguments.length?(b=t%360*mf,k()):b*bf},E.reflectX=function(t){return arguments.length?(m=t?-1:1,k()):m<0},E.reflectY=function(t){return arguments.length?(x=t?-1:1,k()):x<0},E.precision=function(t){return arguments.length?(a=dd(u,S=t*t),C()):zf(S)},E.fitExtent=function(t,n){return ud(E,t,n)},E.fitSize=function(t,n){return cd(E,t,n)},E.fitWidth=function(t,n){return fd(E,t,n)},E.fitHeight=function(t,n){return sd(E,t,n)},function(){return n=t.apply(this,arguments),E.invert=n.invert&&N,k()}}function _d(t){var n=0,e=gf/3,r=vd(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*mf,e=t[1]*mf):[n*bf,e*bf]},i}function bd(t,n){var e=Cf(t),r=(e+Cf(n))/2;if(xf(r)0?n<-yf+df&&(n=-yf+df):n>yf-df&&(n=yf-df);var e=i/kf(Nd(n),r);return[e*Cf(r*t),i-e*Tf(r*t)]}return o.invert=function(t,n){var e=i-n,o=Pf(r)*zf(t*t+e*e),a=Mf(t,xf(e))*Pf(e);return e*r<0&&(a-=gf*Pf(t)*Pf(e)),[a/r,2*wf(kf(i/o,1/r))-yf]},o}function Cd(t,n){return[t,n]}function Pd(t,n){var e=Tf(t),r=t===n?Cf(t):(e-Tf(n))/(n-t),i=e/r+t;if(xf(r)=0;)n+=e[r].value;else n=1;t.value=n}function Gd(t,n){t instanceof Map?(t=[void 0,t],void 0===n&&(n=Wd)):void 0===n&&(n=Vd);for(var e,r,i,o,a,u=new Qd(t),c=[u];e=c.pop();)if((i=n(e.data))&&(a=(i=Array.from(i)).length))for(e.children=i,o=a-1;o>=0;--o)c.push(r=i[o]=new Qd(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(Kd)}function Vd(t){return t.children}function Wd(t){return Array.isArray(t)?t[1]:null}function Zd(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function Kd(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Qd(t){this.data=t,this.depth=this.height=0,this.parent=null}function Jd(t){return null==t?null:tp(t)}function tp(t){if("function"!=typeof t)throw new Error;return t}function np(){return 0}function ep(t){return function(){return t}}qd.invert=function(t,n){for(var e,r=n,i=r*r,o=i*i*i,a=0;a<12&&(o=(i=(r-=e=(r*(zd+$d*i+o*(Dd+Rd*i))-n)/(zd+3*$d*i+o*(7*Dd+9*Rd*i)))*r)*i*i,!(xf(e)df&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},Od.invert=Md(Rf),Bd.invert=Md((function(t){return 2*wf(t)})),Yd.invert=function(t,n){return[-n,2*wf(Sf(t))-yf]},Qd.prototype=Gd.prototype={constructor:Qd,count:function(){return this.eachAfter(Xd)},each:function(t,n){let e=-1;for(const r of this)t.call(n,r,++e,this);return this},eachAfter:function(t,n){for(var e,r,i,o=this,a=[o],u=[],c=-1;o=a.pop();)if(u.push(o),e=o.children)for(r=0,i=e.length;r=0;--r)o.push(e[r]);return this},find:function(t,n){let e=-1;for(const r of this)if(t.call(n,r,++e,this))return r},sum:function(t){return this.eachAfter((function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e}))},sort:function(t){return this.eachBefore((function(n){n.children&&n.children.sort(t)}))},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;t=e.pop(),n=r.pop();for(;t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(n){n.children||t.push(n)})),t},links:function(){var t=this,n=[];return t.each((function(e){e!==t&&n.push({source:e.parent,target:e})})),n},copy:function(){return Gd(this).eachBefore(Zd)},[Symbol.iterator]:function*(){var t,n,e,r,i=this,o=[i];do{for(t=o.reverse(),o=[];i=t.pop();)if(yield i,n=i.children)for(e=0,r=n.length;e(t=(rp*t+ip)%op)/op}function up(t,n){for(var e,r,i=0,o=(t=function(t,n){let e,r,i=t.length;for(;i;)r=n()*i--|0,e=t[i],t[i]=t[r],t[r]=e;return t}(Array.from(t),n)).length,a=[];i0&&e*e>r*r+i*i}function lp(t,n){for(var e=0;e1e-6?(E+Math.sqrt(E*E-4*S*N))/(2*S):N/E);return{x:r+w+M*k,y:i+T+A*k,r:k}}function gp(t,n,e){var r,i,o,a,u=t.x-n.x,c=t.y-n.y,f=u*u+c*c;f?(i=n.r+e.r,i*=i,a=t.r+e.r,i>(a*=a)?(r=(f+a-i)/(2*f),o=Math.sqrt(Math.max(0,a/f-r*r)),e.x=t.x-r*u-o*c,e.y=t.y-r*c+o*u):(r=(f+i-a)/(2*f),o=Math.sqrt(Math.max(0,i/f-r*r)),e.x=n.x+r*u-o*c,e.y=n.y+r*c+o*u)):(e.x=n.x+e.r,e.y=n.y)}function yp(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function vp(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function _p(t){this._=t,this.next=null,this.previous=null}function bp(t,n){if(!(o=(t=function(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}(t)).length))return 0;var e,r,i,o,a,u,c,f,s,l,h;if((e=t[0]).x=0,e.y=0,!(o>1))return e.r;if(r=t[1],e.x=-r.r,r.x=e.r,r.y=0,!(o>2))return e.r+r.r;gp(r,e,i=t[2]),e=new _p(e),r=new _p(r),i=new _p(i),e.next=i.previous=r,r.next=e.previous=i,i.next=r.previous=e;t:for(c=3;c1&&!zp(t,n););return t.slice(0,n)}function zp(t,n){if("/"===t[n]){let e=0;for(;n>0&&"\\"===t[--n];)++e;if(!(1&e))return!0}return!1}function $p(t,n){return t.parent===n.parent?1:2}function Dp(t){var n=t.children;return n?n[0]:t.t}function Rp(t){var n=t.children;return n?n[n.length-1]:t.t}function Fp(t,n,e){var r=e/(n.i-t.i);n.c-=r,n.s+=e,t.c+=r,n.z+=e,n.m+=e}function qp(t,n,e){return t.a.parent===n.parent?t.a:e}function Up(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function Ip(t,n,e,r,i){for(var o,a=t.children,u=-1,c=a.length,f=t.value&&(i-e)/t.value;++uh&&(h=u),y=s*s*g,(d=Math.max(h/y,y/l))>p){s-=u;break}p=d}v.push(a={value:s,dice:c1?n:1)},e}(Op);var Lp=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,c,f,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(Op);function jp(t,n,e){return(n[0]-t[0])*(e[1]-t[1])-(n[1]-t[1])*(e[0]-t[0])}function Hp(t,n){return t[0]-n[0]||t[1]-n[1]}function Xp(t){const n=t.length,e=[0,1];let r,i=2;for(r=2;r1&&jp(t[e[i-2]],t[e[i-1]],t[r])<=0;)--i;e[i++]=r}return e.slice(0,i)}var Gp=Math.random,Vp=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(Gp),Wp=function t(n){function e(t,e){return arguments.length<2&&(e=t,t=0),t=Math.floor(t),e=Math.floor(e)-t,function(){return Math.floor(n()*e+t)}}return e.source=t,e}(Gp),Zp=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(Gp),Kp=function t(n){var e=Zp.source(n);function r(){var t=e.apply(this,arguments);return function(){return Math.exp(t())}}return r.source=t,r}(Gp),Qp=function t(n){function e(t){return(t=+t)<=0?()=>0:function(){for(var e=0,r=t;r>1;--r)e+=n();return e+r*n()}}return e.source=t,e}(Gp),Jp=function t(n){var e=Qp.source(n);function r(t){if(0==(t=+t))return n;var r=e(t);return function(){return r()/t}}return r.source=t,r}(Gp),tg=function t(n){function e(t){return function(){return-Math.log1p(-n())/t}}return e.source=t,e}(Gp),ng=function t(n){function e(t){if((t=+t)<0)throw new RangeError("invalid alpha");return t=1/-t,function(){return Math.pow(1-n(),t)}}return e.source=t,e}(Gp),eg=function t(n){function e(t){if((t=+t)<0||t>1)throw new RangeError("invalid p");return function(){return Math.floor(n()+t)}}return e.source=t,e}(Gp),rg=function t(n){function e(t){if((t=+t)<0||t>1)throw new RangeError("invalid p");return 0===t?()=>1/0:1===t?()=>1:(t=Math.log1p(-t),function(){return 1+Math.floor(Math.log1p(-n())/t)})}return e.source=t,e}(Gp),ig=function t(n){var e=Zp.source(n)();function r(t,r){if((t=+t)<0)throw new RangeError("invalid k");if(0===t)return()=>0;if(r=null==r?1:+r,1===t)return()=>-Math.log1p(-n())*r;var i=(t<1?t+1:t)-1/3,o=1/(3*Math.sqrt(i)),a=t<1?()=>Math.pow(n(),1/t):()=>1;return function(){do{do{var t=e(),u=1+o*t}while(u<=0);u*=u*u;var c=1-n()}while(c>=1-.0331*t*t*t*t&&Math.log(c)>=.5*t*t+i*(1-u+Math.log(u)));return i*u*a()*r}}return r.source=t,r}(Gp),og=function t(n){var e=ig.source(n);function r(t,n){var r=e(t),i=e(n);return function(){var t=r();return 0===t?0:t/(t+i())}}return r.source=t,r}(Gp),ag=function t(n){var e=rg.source(n),r=og.source(n);function i(t,n){return t=+t,(n=+n)>=1?()=>t:n<=0?()=>0:function(){for(var i=0,o=t,a=n;o*a>16&&o*(1-a)>16;){var u=Math.floor((o+1)*a),c=r(u,o-u+1)();c<=a?(i+=u,o-=u,a=(a-c)/(1-c)):(o=u-1,a/=c)}for(var f=a<.5,s=e(f?a:1-a),l=s(),h=0;l<=o;++h)l+=s();return i+(f?h:o-h)}}return i.source=t,i}(Gp),ug=function t(n){function e(t,e,r){var i;return 0==(t=+t)?i=t=>-Math.log(t):(t=1/t,i=n=>Math.pow(n,t)),e=null==e?0:+e,r=null==r?1:+r,function(){return e+r*i(-Math.log1p(-n()))}}return e.source=t,e}(Gp),cg=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,function(){return t+e*Math.tan(Math.PI*n())}}return e.source=t,e}(Gp),fg=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,function(){var r=n();return t+e*Math.log(r/(1-r))}}return e.source=t,e}(Gp),sg=function t(n){var e=ig.source(n),r=ag.source(n);function i(t){return function(){for(var i=0,o=t;o>16;){var a=Math.floor(.875*o),u=e(a)();if(u>o)return i+r(a-1,o/u)();i+=a,o-=u}for(var c=-Math.log1p(-n()),f=0;c<=o;++f)c-=Math.log1p(-n());return i+f}}return i.source=t,i}(Gp);const lg=1/4294967296;function hg(t,n){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(n).domain(t)}return this}function dg(t,n){switch(arguments.length){case 0:break;case 1:"function"==typeof t?this.interpolator(t):this.range(t);break;default:this.domain(t),"function"==typeof n?this.interpolator(n):this.range(n)}return this}const pg=Symbol("implicit");function gg(){var t=new InternMap,n=[],e=[],r=pg;function i(i){let o=t.get(i);if(void 0===o){if(r!==pg)return r;t.set(i,o=n.push(i)-1)}return e[o%e.length]}return i.domain=function(e){if(!arguments.length)return n.slice();n=[],t=new InternMap;for(const r of e)t.has(r)||t.set(r,n.push(r)-1);return i},i.range=function(t){return arguments.length?(e=Array.from(t),i):e.slice()},i.unknown=function(t){return arguments.length?(r=t,i):r},i.copy=function(){return gg(n,e).unknown(r)},hg.apply(i,arguments),i}function yg(){var t,n,e=gg().unknown(void 0),r=e.domain,i=e.range,o=0,a=1,u=!1,c=0,f=0,s=.5;function l(){var e=r().length,l=an&&(e=t,t=n,n=e),function(e){return Math.max(t,Math.min(n,e))}}(a[0],a[t-1])),r=t>2?Mg:wg,i=o=null,l}function l(n){return null==n||isNaN(n=+n)?e:(i||(i=r(a.map(t),u,c)))(t(f(n)))}return l.invert=function(e){return f(n((o||(o=r(u,a.map(t),Yr)))(e)))},l.domain=function(t){return arguments.length?(a=Array.from(t,_g),s()):a.slice()},l.range=function(t){return arguments.length?(u=Array.from(t),s()):u.slice()},l.rangeRound=function(t){return u=Array.from(t),c=Vr,s()},l.clamp=function(t){return arguments.length?(f=!!t||mg,s()):f!==mg},l.interpolate=function(t){return arguments.length?(c=t,s()):c},l.unknown=function(t){return arguments.length?(e=t,l):e},function(e,r){return t=e,n=r,s()}}function Sg(){return Ag()(mg,mg)}function Eg(n,e,r,i){var o,a=W(n,e,r);switch((i=Jc(null==i?",f":i)).type){case"s":var u=Math.max(Math.abs(n),Math.abs(e));return null!=i.precision||isNaN(o=lf(a,u))||(i.precision=o),t.formatPrefix(i,u);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=hf(a,Math.max(Math.abs(n),Math.abs(e))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=sf(a))||(i.precision=o-2*("%"===i.type))}return t.format(i)}function Ng(t){var n=t.domain;return t.ticks=function(t){var e=n();return G(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){var r=n();return Eg(r[0],r[r.length-1],null==t?10:t,e)},t.nice=function(e){null==e&&(e=10);var r,i,o=n(),a=0,u=o.length-1,c=o[a],f=o[u],s=10;for(f0;){if((i=V(c,f,e))===r)return o[a]=c,o[u]=f,n(o);if(i>0)c=Math.floor(c/i)*i,f=Math.ceil(f/i)*i;else{if(!(i<0))break;c=Math.ceil(c*i)/i,f=Math.floor(f*i)/i}r=i}return t},t}function kg(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a-t(-n,e)}function Fg(n){const e=n(Cg,Pg),r=e.domain;let i,o,a=10;function u(){return i=function(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),n=>Math.log(n)/t)}(a),o=function(t){return 10===t?Dg:t===Math.E?Math.exp:n=>Math.pow(t,n)}(a),r()[0]<0?(i=Rg(i),o=Rg(o),n(zg,$g)):n(Cg,Pg),e}return e.base=function(t){return arguments.length?(a=+t,u()):a},e.domain=function(t){return arguments.length?(r(t),u()):r()},e.ticks=t=>{const n=r();let e=n[0],u=n[n.length-1];const c=u0){for(;l<=h;++l)for(f=1;fu)break;p.push(s)}}else for(;l<=h;++l)for(f=a-1;f>=1;--f)if(s=l>0?f/o(-l):f*o(l),!(su)break;p.push(s)}2*p.length{if(null==n&&(n=10),null==r&&(r=10===a?"s":","),"function"!=typeof r&&(a%1||null!=(r=Jc(r)).precision||(r.trim=!0),r=t.format(r)),n===1/0)return r;const u=Math.max(1,a*n/e.ticks().length);return t=>{let n=t/o(Math.round(i(t)));return n*ar(kg(r(),{floor:t=>o(Math.floor(i(t))),ceil:t=>o(Math.ceil(i(t)))})),e}function qg(t){return function(n){return Math.sign(n)*Math.log1p(Math.abs(n/t))}}function Ug(t){return function(n){return Math.sign(n)*Math.expm1(Math.abs(n))*t}}function Ig(t){var n=1,e=t(qg(n),Ug(n));return e.constant=function(e){return arguments.length?t(qg(n=+e),Ug(n)):n},Ng(e)}function Og(t){return function(n){return n<0?-Math.pow(-n,t):Math.pow(n,t)}}function Bg(t){return t<0?-Math.sqrt(-t):Math.sqrt(t)}function Yg(t){return t<0?-t*t:t*t}function Lg(t){var n=t(mg,mg),e=1;return n.exponent=function(n){return arguments.length?1===(e=+n)?t(mg,mg):.5===e?t(Bg,Yg):t(Og(e),Og(1/e)):e},Ng(n)}function jg(){var t=Lg(Ag());return t.copy=function(){return Tg(t,jg()).exponent(t.exponent())},hg.apply(t,arguments),t}function Hg(t){return Math.sign(t)*t*t}const Xg=new Date,Gg=new Date;function Vg(t,n,e,r){function i(n){return t(n=0===arguments.length?new Date:new Date(+n)),n}return i.floor=n=>(t(n=new Date(+n)),n),i.ceil=e=>(t(e=new Date(e-1)),n(e,1),t(e),e),i.round=t=>{const n=i(t),e=i.ceil(t);return t-n(n(t=new Date(+t),null==e?1:Math.floor(e)),t),i.range=(e,r,o)=>{const a=[];if(e=i.ceil(e),o=null==o?1:Math.floor(o),!(e0))return a;let u;do{a.push(u=new Date(+e)),n(e,o),t(e)}while(uVg((n=>{if(n>=n)for(;t(n),!e(n);)n.setTime(n-1)}),((t,r)=>{if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})),e&&(i.count=(n,r)=>(Xg.setTime(+n),Gg.setTime(+r),t(Xg),t(Gg),Math.floor(e(Xg,Gg))),i.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?n=>r(n)%t==0:n=>i.count(0,n)%t==0):i:null)),i}const Wg=Vg((()=>{}),((t,n)=>{t.setTime(+t+n)}),((t,n)=>n-t));Wg.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?Vg((n=>{n.setTime(Math.floor(n/t)*t)}),((n,e)=>{n.setTime(+n+e*t)}),((n,e)=>(e-n)/t)):Wg:null);const Zg=Wg.range,Kg=1e3,Qg=6e4,Jg=36e5,ty=864e5,ny=6048e5,ey=2592e6,ry=31536e6,iy=Vg((t=>{t.setTime(t-t.getMilliseconds())}),((t,n)=>{t.setTime(+t+n*Kg)}),((t,n)=>(n-t)/Kg),(t=>t.getUTCSeconds())),oy=iy.range,ay=Vg((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*Kg)}),((t,n)=>{t.setTime(+t+n*Qg)}),((t,n)=>(n-t)/Qg),(t=>t.getMinutes())),uy=ay.range,cy=Vg((t=>{t.setUTCSeconds(0,0)}),((t,n)=>{t.setTime(+t+n*Qg)}),((t,n)=>(n-t)/Qg),(t=>t.getUTCMinutes())),fy=cy.range,sy=Vg((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*Kg-t.getMinutes()*Qg)}),((t,n)=>{t.setTime(+t+n*Jg)}),((t,n)=>(n-t)/Jg),(t=>t.getHours())),ly=sy.range,hy=Vg((t=>{t.setUTCMinutes(0,0,0)}),((t,n)=>{t.setTime(+t+n*Jg)}),((t,n)=>(n-t)/Jg),(t=>t.getUTCHours())),dy=hy.range,py=Vg((t=>t.setHours(0,0,0,0)),((t,n)=>t.setDate(t.getDate()+n)),((t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qg)/ty),(t=>t.getDate()-1)),gy=py.range,yy=Vg((t=>{t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+n)}),((t,n)=>(n-t)/ty),(t=>t.getUTCDate()-1)),vy=yy.range,_y=Vg((t=>{t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+n)}),((t,n)=>(n-t)/ty),(t=>Math.floor(t/ty))),by=_y.range;function my(t){return Vg((n=>{n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)}),((t,n)=>{t.setDate(t.getDate()+7*n)}),((t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qg)/ny))}const xy=my(0),wy=my(1),My=my(2),Ty=my(3),Ay=my(4),Sy=my(5),Ey=my(6),Ny=xy.range,ky=wy.range,Cy=My.range,Py=Ty.range,zy=Ay.range,$y=Sy.range,Dy=Ey.range;function Ry(t){return Vg((n=>{n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+7*n)}),((t,n)=>(n-t)/ny))}const Fy=Ry(0),qy=Ry(1),Uy=Ry(2),Iy=Ry(3),Oy=Ry(4),By=Ry(5),Yy=Ry(6),Ly=Fy.range,jy=qy.range,Hy=Uy.range,Xy=Iy.range,Gy=Oy.range,Vy=By.range,Wy=Yy.range,Zy=Vg((t=>{t.setDate(1),t.setHours(0,0,0,0)}),((t,n)=>{t.setMonth(t.getMonth()+n)}),((t,n)=>n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())),(t=>t.getMonth())),Ky=Zy.range,Qy=Vg((t=>{t.setUTCDate(1),t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCMonth(t.getUTCMonth()+n)}),((t,n)=>n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())),(t=>t.getUTCMonth())),Jy=Qy.range,tv=Vg((t=>{t.setMonth(0,1),t.setHours(0,0,0,0)}),((t,n)=>{t.setFullYear(t.getFullYear()+n)}),((t,n)=>n.getFullYear()-t.getFullYear()),(t=>t.getFullYear()));tv.every=t=>isFinite(t=Math.floor(t))&&t>0?Vg((n=>{n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)}),((n,e)=>{n.setFullYear(n.getFullYear()+e*t)})):null;const nv=tv.range,ev=Vg((t=>{t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCFullYear(t.getUTCFullYear()+n)}),((t,n)=>n.getUTCFullYear()-t.getUTCFullYear()),(t=>t.getUTCFullYear()));ev.every=t=>isFinite(t=Math.floor(t))&&t>0?Vg((n=>{n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)}),((n,e)=>{n.setUTCFullYear(n.getUTCFullYear()+e*t)})):null;const rv=ev.range;function iv(t,n,e,i,o,a){const u=[[iy,1,Kg],[iy,5,5e3],[iy,15,15e3],[iy,30,3e4],[a,1,Qg],[a,5,3e5],[a,15,9e5],[a,30,18e5],[o,1,Jg],[o,3,108e5],[o,6,216e5],[o,12,432e5],[i,1,ty],[i,2,1728e5],[e,1,ny],[n,1,ey],[n,3,7776e6],[t,1,ry]];function c(n,e,i){const o=Math.abs(e-n)/i,a=r((([,,t])=>t)).right(u,o);if(a===u.length)return t.every(W(n/ry,e/ry,i));if(0===a)return Wg.every(Math.max(W(n,e,i),1));const[c,f]=u[o/u[a-1][2]=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:k_,s:C_,S:Zv,u:Kv,U:Qv,V:t_,w:n_,W:e_,x:null,X:null,y:r_,Y:o_,Z:u_,"%":N_},m={a:function(t){return a[t.getUTCDay()]},A:function(t){return o[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return u[t.getUTCMonth()]},c:null,d:c_,e:c_,f:d_,g:T_,G:S_,H:f_,I:s_,j:l_,L:h_,m:p_,M:g_,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:k_,s:C_,S:y_,u:v_,U:__,V:m_,w:x_,W:w_,x:null,X:null,y:M_,Y:A_,Z:E_,"%":N_},x={a:function(t,n,e){var r=d.exec(n.slice(e));return r?(t.w=p.get(r[0].toLowerCase()),e+r[0].length):-1},A:function(t,n,e){var r=l.exec(n.slice(e));return r?(t.w=h.get(r[0].toLowerCase()),e+r[0].length):-1},b:function(t,n,e){var r=v.exec(n.slice(e));return r?(t.m=_.get(r[0].toLowerCase()),e+r[0].length):-1},B:function(t,n,e){var r=g.exec(n.slice(e));return r?(t.m=y.get(r[0].toLowerCase()),e+r[0].length):-1},c:function(t,e,r){return T(t,n,e,r)},d:zv,e:zv,f:Uv,g:Nv,G:Ev,H:Dv,I:Dv,j:$v,L:qv,m:Pv,M:Rv,p:function(t,n,e){var r=f.exec(n.slice(e));return r?(t.p=s.get(r[0].toLowerCase()),e+r[0].length):-1},q:Cv,Q:Ov,s:Bv,S:Fv,u:Mv,U:Tv,V:Av,w:wv,W:Sv,x:function(t,n,r){return T(t,e,n,r)},X:function(t,n,e){return T(t,r,n,e)},y:Nv,Y:Ev,Z:kv,"%":Iv};function w(t,n){return function(e){var r,i,o,a=[],u=-1,c=0,f=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in o||(o.w=1),"Z"in o?(i=(r=sv(lv(o.y,0,1))).getUTCDay(),r=i>4||0===i?qy.ceil(r):qy(r),r=yy.offset(r,7*(o.V-1)),o.y=r.getUTCFullYear(),o.m=r.getUTCMonth(),o.d=r.getUTCDate()+(o.w+6)%7):(i=(r=fv(lv(o.y,0,1))).getDay(),r=i>4||0===i?wy.ceil(r):wy(r),r=py.offset(r,7*(o.V-1)),o.y=r.getFullYear(),o.m=r.getMonth(),o.d=r.getDate()+(o.w+6)%7)}else("W"in o||"U"in o)&&("w"in o||(o.w="u"in o?o.u%7:"W"in o?1:0),i="Z"in o?sv(lv(o.y,0,1)).getUTCDay():fv(lv(o.y,0,1)).getDay(),o.m=0,o.d="W"in o?(o.w+6)%7+7*o.W-(i+5)%7:o.w+7*o.U-(i+6)%7);return"Z"in o?(o.H+=o.Z/100|0,o.M+=o.Z%100,sv(o)):fv(o)}}function T(t,n,e,r){for(var i,o,a=0,u=n.length,c=e.length;a=c)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=x[i in pv?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}return b.x=w(e,b),b.X=w(r,b),b.c=w(n,b),m.x=w(e,m),m.X=w(r,m),m.c=w(n,m),{format:function(t){var n=w(t+="",b);return n.toString=function(){return t},n},parse:function(t){var n=M(t+="",!1);return n.toString=function(){return t},n},utcFormat:function(t){var n=w(t+="",m);return n.toString=function(){return t},n},utcParse:function(t){var n=M(t+="",!0);return n.toString=function(){return t},n}}}var dv,pv={"-":"",_:" ",0:"0"},gv=/^\s*\d+/,yv=/^%/,vv=/[\\^$*+?|[\]().{}]/g;function _v(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o[t.toLowerCase(),n])))}function wv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.w=+r[0],e+r[0].length):-1}function Mv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.u=+r[0],e+r[0].length):-1}function Tv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.U=+r[0],e+r[0].length):-1}function Av(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.V=+r[0],e+r[0].length):-1}function Sv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.W=+r[0],e+r[0].length):-1}function Ev(t,n,e){var r=gv.exec(n.slice(e,e+4));return r?(t.y=+r[0],e+r[0].length):-1}function Nv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),e+r[0].length):-1}function kv(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function Cv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.q=3*r[0]-3,e+r[0].length):-1}function Pv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function zv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function $v(t,n,e){var r=gv.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function Dv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function Rv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function Fv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function qv(t,n,e){var r=gv.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function Uv(t,n,e){var r=gv.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function Iv(t,n,e){var r=yv.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function Ov(t,n,e){var r=gv.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function Bv(t,n,e){var r=gv.exec(n.slice(e));return r?(t.s=+r[0],e+r[0].length):-1}function Yv(t,n){return _v(t.getDate(),n,2)}function Lv(t,n){return _v(t.getHours(),n,2)}function jv(t,n){return _v(t.getHours()%12||12,n,2)}function Hv(t,n){return _v(1+py.count(tv(t),t),n,3)}function Xv(t,n){return _v(t.getMilliseconds(),n,3)}function Gv(t,n){return Xv(t,n)+"000"}function Vv(t,n){return _v(t.getMonth()+1,n,2)}function Wv(t,n){return _v(t.getMinutes(),n,2)}function Zv(t,n){return _v(t.getSeconds(),n,2)}function Kv(t){var n=t.getDay();return 0===n?7:n}function Qv(t,n){return _v(xy.count(tv(t)-1,t),n,2)}function Jv(t){var n=t.getDay();return n>=4||0===n?Ay(t):Ay.ceil(t)}function t_(t,n){return t=Jv(t),_v(Ay.count(tv(t),t)+(4===tv(t).getDay()),n,2)}function n_(t){return t.getDay()}function e_(t,n){return _v(wy.count(tv(t)-1,t),n,2)}function r_(t,n){return _v(t.getFullYear()%100,n,2)}function i_(t,n){return _v((t=Jv(t)).getFullYear()%100,n,2)}function o_(t,n){return _v(t.getFullYear()%1e4,n,4)}function a_(t,n){var e=t.getDay();return _v((t=e>=4||0===e?Ay(t):Ay.ceil(t)).getFullYear()%1e4,n,4)}function u_(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+_v(n/60|0,"0",2)+_v(n%60,"0",2)}function c_(t,n){return _v(t.getUTCDate(),n,2)}function f_(t,n){return _v(t.getUTCHours(),n,2)}function s_(t,n){return _v(t.getUTCHours()%12||12,n,2)}function l_(t,n){return _v(1+yy.count(ev(t),t),n,3)}function h_(t,n){return _v(t.getUTCMilliseconds(),n,3)}function d_(t,n){return h_(t,n)+"000"}function p_(t,n){return _v(t.getUTCMonth()+1,n,2)}function g_(t,n){return _v(t.getUTCMinutes(),n,2)}function y_(t,n){return _v(t.getUTCSeconds(),n,2)}function v_(t){var n=t.getUTCDay();return 0===n?7:n}function __(t,n){return _v(Fy.count(ev(t)-1,t),n,2)}function b_(t){var n=t.getUTCDay();return n>=4||0===n?Oy(t):Oy.ceil(t)}function m_(t,n){return t=b_(t),_v(Oy.count(ev(t),t)+(4===ev(t).getUTCDay()),n,2)}function x_(t){return t.getUTCDay()}function w_(t,n){return _v(qy.count(ev(t)-1,t),n,2)}function M_(t,n){return _v(t.getUTCFullYear()%100,n,2)}function T_(t,n){return _v((t=b_(t)).getUTCFullYear()%100,n,2)}function A_(t,n){return _v(t.getUTCFullYear()%1e4,n,4)}function S_(t,n){var e=t.getUTCDay();return _v((t=e>=4||0===e?Oy(t):Oy.ceil(t)).getUTCFullYear()%1e4,n,4)}function E_(){return"+0000"}function N_(){return"%"}function k_(t){return+t}function C_(t){return Math.floor(+t/1e3)}function P_(n){return dv=hv(n),t.timeFormat=dv.format,t.timeParse=dv.parse,t.utcFormat=dv.utcFormat,t.utcParse=dv.utcParse,dv}t.timeFormat=void 0,t.timeParse=void 0,t.utcFormat=void 0,t.utcParse=void 0,P_({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var z_="%Y-%m-%dT%H:%M:%S.%LZ";var $_=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat(z_),D_=$_;var R_=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse(z_),F_=R_;function q_(t){return new Date(t)}function U_(t){return t instanceof Date?+t:+new Date(+t)}function I_(t,n,e,r,i,o,a,u,c,f){var s=Sg(),l=s.invert,h=s.domain,d=f(".%L"),p=f(":%S"),g=f("%I:%M"),y=f("%I %p"),v=f("%a %d"),_=f("%b %d"),b=f("%B"),m=f("%Y");function x(t){return(c(t)Fr(t[t.length-1]),ib=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(H_),ob=rb(ib),ab=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(H_),ub=rb(ab),cb=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(H_),fb=rb(cb),sb=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(H_),lb=rb(sb),hb=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(H_),db=rb(hb),pb=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(H_),gb=rb(pb),yb=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(H_),vb=rb(yb),_b=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(H_),bb=rb(_b),mb=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(H_),xb=rb(mb),wb=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccece699d8c966c2a42ca25f006d2c","edf8fbccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b").map(H_),Mb=rb(wb),Tb=new Array(3).concat("e0ecf49ebcda8856a7","edf8fbb3cde38c96c688419d","edf8fbb3cde38c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b").map(H_),Ab=rb(Tb),Sb=new Array(3).concat("e0f3dba8ddb543a2ca","f0f9e8bae4bc7bccc42b8cbe","f0f9e8bae4bc7bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081").map(H_),Eb=rb(Sb),Nb=new Array(3).concat("fee8c8fdbb84e34a33","fef0d9fdcc8afc8d59d7301f","fef0d9fdcc8afc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000").map(H_),kb=rb(Nb),Cb=new Array(3).concat("ece2f0a6bddb1c9099","f6eff7bdc9e167a9cf02818a","f6eff7bdc9e167a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636").map(H_),Pb=rb(Cb),zb=new Array(3).concat("ece7f2a6bddb2b8cbe","f1eef6bdc9e174a9cf0570b0","f1eef6bdc9e174a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858").map(H_),$b=rb(zb),Db=new Array(3).concat("e7e1efc994c7dd1c77","f1eef6d7b5d8df65b0ce1256","f1eef6d7b5d8df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f").map(H_),Rb=rb(Db),Fb=new Array(3).concat("fde0ddfa9fb5c51b8a","feebe2fbb4b9f768a1ae017e","feebe2fbb4b9f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a").map(H_),qb=rb(Fb),Ub=new Array(3).concat("edf8b17fcdbb2c7fb8","ffffcca1dab441b6c4225ea8","ffffcca1dab441b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58").map(H_),Ib=rb(Ub),Ob=new Array(3).concat("f7fcb9addd8e31a354","ffffccc2e69978c679238443","ffffccc2e69978c67931a354006837","ffffccd9f0a3addd8e78c67931a354006837","ffffccd9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529").map(H_),Bb=rb(Ob),Yb=new Array(3).concat("fff7bcfec44fd95f0e","ffffd4fed98efe9929cc4c02","ffffd4fed98efe9929d95f0e993404","ffffd4fee391fec44ffe9929d95f0e993404","ffffd4fee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506").map(H_),Lb=rb(Yb),jb=new Array(3).concat("ffeda0feb24cf03b20","ffffb2fecc5cfd8d3ce31a1c","ffffb2fecc5cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026").map(H_),Hb=rb(jb),Xb=new Array(3).concat("deebf79ecae13182bd","eff3ffbdd7e76baed62171b5","eff3ffbdd7e76baed63182bd08519c","eff3ffc6dbef9ecae16baed63182bd08519c","eff3ffc6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b").map(H_),Gb=rb(Xb),Vb=new Array(3).concat("e5f5e0a1d99b31a354","edf8e9bae4b374c476238b45","edf8e9bae4b374c47631a354006d2c","edf8e9c7e9c0a1d99b74c47631a354006d2c","edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b").map(H_),Wb=rb(Vb),Zb=new Array(3).concat("f0f0f0bdbdbd636363","f7f7f7cccccc969696525252","f7f7f7cccccc969696636363252525","f7f7f7d9d9d9bdbdbd969696636363252525","f7f7f7d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000").map(H_),Kb=rb(Zb),Qb=new Array(3).concat("efedf5bcbddc756bb1","f2f0f7cbc9e29e9ac86a51a3","f2f0f7cbc9e29e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d").map(H_),Jb=rb(Qb),tm=new Array(3).concat("fee0d2fc9272de2d26","fee5d9fcae91fb6a4acb181d","fee5d9fcae91fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d").map(H_),nm=rb(tm),em=new Array(3).concat("fee6cefdae6be6550d","feeddefdbe85fd8d3cd94701","feeddefdbe85fd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704").map(H_),rm=rb(em);var im=hi(Tr(300,.5,0),Tr(-240,.5,1)),om=hi(Tr(-100,.75,.35),Tr(80,1.5,.8)),am=hi(Tr(260,.75,.35),Tr(80,1.5,.8)),um=Tr();var cm=Fe(),fm=Math.PI/3,sm=2*Math.PI/3;function lm(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}}var hm=lm(H_("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),dm=lm(H_("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),pm=lm(H_("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),gm=lm(H_("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));function ym(t){return function(){return t}}const vm=Math.abs,_m=Math.atan2,bm=Math.cos,mm=Math.max,xm=Math.min,wm=Math.sin,Mm=Math.sqrt,Tm=1e-12,Am=Math.PI,Sm=Am/2,Em=2*Am;function Nm(t){return t>=1?Sm:t<=-1?-Sm:Math.asin(t)}function km(t){let n=3;return t.digits=function(e){if(!arguments.length)return n;if(null==e)n=null;else{const t=Math.floor(e);if(!(t>=0))throw new RangeError(`invalid digits: ${e}`);n=t}return t},()=>new Ua(n)}function Cm(t){return t.innerRadius}function Pm(t){return t.outerRadius}function zm(t){return t.startAngle}function $m(t){return t.endAngle}function Dm(t){return t&&t.padAngle}function Rm(t,n,e,r,i,o,a){var u=t-e,c=n-r,f=(a?o:-o)/Mm(u*u+c*c),s=f*c,l=-f*u,h=t+s,d=n+l,p=e+s,g=r+l,y=(h+p)/2,v=(d+g)/2,_=p-h,b=g-d,m=_*_+b*b,x=i-o,w=h*g-p*d,M=(b<0?-1:1)*Mm(mm(0,x*x*m-w*w)),T=(w*b-_*M)/m,A=(-w*_-b*M)/m,S=(w*b+_*M)/m,E=(-w*_+b*M)/m,N=T-y,k=A-v,C=S-y,P=E-v;return N*N+k*k>C*C+P*P&&(T=S,A=E),{cx:T,cy:A,x01:-s,y01:-l,x11:T*(i/x-1),y11:A*(i/x-1)}}var Fm=Array.prototype.slice;function qm(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}function Um(t){this._context=t}function Im(t){return new Um(t)}function Om(t){return t[0]}function Bm(t){return t[1]}function Ym(t,n){var e=ym(!0),r=null,i=Im,o=null,a=km(u);function u(u){var c,f,s,l=(u=qm(u)).length,h=!1;for(null==r&&(o=i(s=a())),c=0;c<=l;++c)!(c=l;--h)u.point(v[h],_[h]);u.lineEnd(),u.areaEnd()}y&&(v[s]=+t(d,s,f),_[s]=+n(d,s,f),u.point(r?+r(d,s,f):v[s],e?+e(d,s,f):_[s]))}if(p)return u=null,p+""||null}function s(){return Ym().defined(i).curve(a).context(o)}return t="function"==typeof t?t:void 0===t?Om:ym(+t),n="function"==typeof n?n:ym(void 0===n?0:+n),e="function"==typeof e?e:void 0===e?Bm:ym(+e),f.x=function(n){return arguments.length?(t="function"==typeof n?n:ym(+n),r=null,f):t},f.x0=function(n){return arguments.length?(t="function"==typeof n?n:ym(+n),f):t},f.x1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:ym(+t),f):r},f.y=function(t){return arguments.length?(n="function"==typeof t?t:ym(+t),e=null,f):n},f.y0=function(t){return arguments.length?(n="function"==typeof t?t:ym(+t),f):n},f.y1=function(t){return arguments.length?(e=null==t?null:"function"==typeof t?t:ym(+t),f):e},f.lineX0=f.lineY0=function(){return s().x(t).y(n)},f.lineY1=function(){return s().x(t).y(e)},f.lineX1=function(){return s().x(r).y(n)},f.defined=function(t){return arguments.length?(i="function"==typeof t?t:ym(!!t),f):i},f.curve=function(t){return arguments.length?(a=t,null!=o&&(u=a(o)),f):a},f.context=function(t){return arguments.length?(null==t?o=u=null:u=a(o=t),f):o},f}function jm(t,n){return nt?1:n>=t?0:NaN}function Hm(t){return t}Um.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var Xm=Vm(Im);function Gm(t){this._curve=t}function Vm(t){function n(n){return new Gm(t(n))}return n._curve=t,n}function Wm(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(Vm(t)):n()._curve},t}function Zm(){return Wm(Ym().curve(Xm))}function Km(){var t=Lm().curve(Xm),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Wm(e())},delete t.lineX0,t.lineEndAngle=function(){return Wm(r())},delete t.lineX1,t.lineInnerRadius=function(){return Wm(i())},delete t.lineY0,t.lineOuterRadius=function(){return Wm(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(Vm(t)):n()._curve},t}function Qm(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}Gm.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};class Jm{constructor(t,n){this._context=t,this._x=n}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line}point(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,n,t,n):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+n)/2,t,this._y0,t,n)}this._x0=t,this._y0=n}}class tx{constructor(t){this._context=t}lineStart(){this._point=0}lineEnd(){}point(t,n){if(t=+t,n=+n,0===this._point)this._point=1;else{const e=Qm(this._x0,this._y0),r=Qm(this._x0,this._y0=(this._y0+n)/2),i=Qm(t,this._y0),o=Qm(t,n);this._context.moveTo(...e),this._context.bezierCurveTo(...r,...i,...o)}this._x0=t,this._y0=n}}function nx(t){return new Jm(t,!0)}function ex(t){return new Jm(t,!1)}function rx(t){return new tx(t)}function ix(t){return t.source}function ox(t){return t.target}function ax(t){let n=ix,e=ox,r=Om,i=Bm,o=null,a=null,u=km(c);function c(){let c;const f=Fm.call(arguments),s=n.apply(this,f),l=e.apply(this,f);if(null==o&&(a=t(c=u())),a.lineStart(),f[0]=s,a.point(+r.apply(this,f),+i.apply(this,f)),f[0]=l,a.point(+r.apply(this,f),+i.apply(this,f)),a.lineEnd(),c)return a=null,c+""||null}return c.source=function(t){return arguments.length?(n=t,c):n},c.target=function(t){return arguments.length?(e=t,c):e},c.x=function(t){return arguments.length?(r="function"==typeof t?t:ym(+t),c):r},c.y=function(t){return arguments.length?(i="function"==typeof t?t:ym(+t),c):i},c.context=function(n){return arguments.length?(null==n?o=a=null:a=t(o=n),c):o},c}const ux=Mm(3);var cx={draw(t,n){const e=.59436*Mm(n+xm(n/28,.75)),r=e/2,i=r*ux;t.moveTo(0,e),t.lineTo(0,-e),t.moveTo(-i,-r),t.lineTo(i,r),t.moveTo(-i,r),t.lineTo(i,-r)}},fx={draw(t,n){const e=Mm(n/Am);t.moveTo(e,0),t.arc(0,0,e,0,Em)}},sx={draw(t,n){const e=Mm(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}};const lx=Mm(1/3),hx=2*lx;var dx={draw(t,n){const e=Mm(n/hx),r=e*lx;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},px={draw(t,n){const e=.62625*Mm(n);t.moveTo(0,-e),t.lineTo(e,0),t.lineTo(0,e),t.lineTo(-e,0),t.closePath()}},gx={draw(t,n){const e=.87559*Mm(n-xm(n/7,2));t.moveTo(-e,0),t.lineTo(e,0),t.moveTo(0,e),t.lineTo(0,-e)}},yx={draw(t,n){const e=Mm(n),r=-e/2;t.rect(r,r,e,e)}},vx={draw(t,n){const e=.4431*Mm(n);t.moveTo(e,e),t.lineTo(e,-e),t.lineTo(-e,-e),t.lineTo(-e,e),t.closePath()}};const _x=wm(Am/10)/wm(7*Am/10),bx=wm(Em/10)*_x,mx=-bm(Em/10)*_x;var xx={draw(t,n){const e=Mm(.8908130915292852*n),r=bx*e,i=mx*e;t.moveTo(0,-e),t.lineTo(r,i);for(let n=1;n<5;++n){const o=Em*n/5,a=bm(o),u=wm(o);t.lineTo(u*e,-a*e),t.lineTo(a*r-u*i,u*r+a*i)}t.closePath()}};const wx=Mm(3);var Mx={draw(t,n){const e=-Mm(n/(3*wx));t.moveTo(0,2*e),t.lineTo(-wx*e,-e),t.lineTo(wx*e,-e),t.closePath()}};const Tx=Mm(3);var Ax={draw(t,n){const e=.6824*Mm(n),r=e/2,i=e*Tx/2;t.moveTo(0,-e),t.lineTo(i,r),t.lineTo(-i,r),t.closePath()}};const Sx=-.5,Ex=Mm(3)/2,Nx=1/Mm(12),kx=3*(Nx/2+1);var Cx={draw(t,n){const e=Mm(n/kx),r=e/2,i=e*Nx,o=r,a=e*Nx+e,u=-o,c=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,c),t.lineTo(Sx*r-Ex*i,Ex*r+Sx*i),t.lineTo(Sx*o-Ex*a,Ex*o+Sx*a),t.lineTo(Sx*u-Ex*c,Ex*u+Sx*c),t.lineTo(Sx*r+Ex*i,Sx*i-Ex*r),t.lineTo(Sx*o+Ex*a,Sx*a-Ex*o),t.lineTo(Sx*u+Ex*c,Sx*c-Ex*u),t.closePath()}},Px={draw(t,n){const e=.6189*Mm(n-xm(n/6,1.7));t.moveTo(-e,-e),t.lineTo(e,e),t.moveTo(-e,e),t.lineTo(e,-e)}};const zx=[fx,sx,dx,yx,xx,Mx,Cx],$x=[fx,gx,Px,Ax,cx,vx,px];function Dx(){}function Rx(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function Fx(t){this._context=t}function qx(t){this._context=t}function Ux(t){this._context=t}function Ix(t,n){this._basis=new Fx(t),this._beta=n}Fx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Rx(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Rx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},qx.prototype={areaStart:Dx,areaEnd:Dx,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:Rx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Ux.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:Rx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Ix.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*a),this._beta*n[c]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var Ox=function t(n){function e(t){return 1===n?new Fx(t):new Ix(t,n)}return e.beta=function(n){return t(+n)},e}(.85);function Bx(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Yx(t,n){this._context=t,this._k=(1-n)/6}Yx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Bx(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Bx(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Lx=function t(n){function e(t){return new Yx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function jx(t,n){this._context=t,this._k=(1-n)/6}jx.prototype={areaStart:Dx,areaEnd:Dx,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Bx(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Hx=function t(n){function e(t){return new jx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function Xx(t,n){this._context=t,this._k=(1-n)/6}Xx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Bx(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Gx=function t(n){function e(t){return new Xx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function Vx(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>Tm){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>Tm){var f=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*f+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*f+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function Wx(t,n){this._context=t,this._alpha=n}Wx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Vx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Zx=function t(n){function e(t){return n?new Wx(t,n):new Yx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function Kx(t,n){this._context=t,this._alpha=n}Kx.prototype={areaStart:Dx,areaEnd:Dx,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Vx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Qx=function t(n){function e(t){return n?new Kx(t,n):new jx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function Jx(t,n){this._context=t,this._alpha=n}Jx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Vx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var tw=function t(n){function e(t){return n?new Jx(t,n):new Xx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function nw(t){this._context=t}function ew(t){return t<0?-1:1}function rw(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(ew(o)+ew(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function iw(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function ow(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function aw(t){this._context=t}function uw(t){this._context=new cw(t)}function cw(t){this._context=t}function fw(t){this._context=t}function sw(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function pw(t,n){return t[n]}function gw(t){const n=[];return n.key=t,n}function yw(t){var n=t.map(vw);return dw(t).sort((function(t,e){return n[t]-n[e]}))}function vw(t){for(var n,e=-1,r=0,i=t.length,o=-1/0;++eo&&(o=n,r=e);return r}function _w(t){var n=t.map(bw);return dw(t).sort((function(t,e){return n[t]-n[e]}))}function bw(t){for(var n,e=0,r=-1,i=t.length;++r=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var mw=t=>()=>t;function xw(t,{sourceEvent:n,target:e,transform:r,dispatch:i}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:i}})}function ww(t,n,e){this.k=t,this.x=n,this.y=e}ww.prototype={constructor:ww,scale:function(t){return 1===t?this:new ww(this.k*t,this.x,this.y)},translate:function(t,n){return 0===t&0===n?this:new ww(this.k,this.x+this.k*t,this.y+this.k*n)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Mw=new ww(1,0,0);function Tw(t){for(;!t.__zoom;)if(!(t=t.parentNode))return Mw;return t.__zoom}function Aw(t){t.stopImmediatePropagation()}function Sw(t){t.preventDefault(),t.stopImmediatePropagation()}function Ew(t){return!(t.ctrlKey&&"wheel"!==t.type||t.button)}function Nw(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t).hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]:[[0,0],[t.clientWidth,t.clientHeight]]}function kw(){return this.__zoom||Mw}function Cw(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function Pw(){return navigator.maxTouchPoints||"ontouchstart"in this}function zw(t,n,e){var r=t.invertX(n[0][0])-e[0][0],i=t.invertX(n[1][0])-e[1][0],o=t.invertY(n[0][1])-e[0][1],a=t.invertY(n[1][1])-e[1][1];return t.translate(i>r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}Tw.prototype=ww.prototype,t.Adder=T,t.Delaunay=Lu,t.FormatSpecifier=tf,t.InternMap=InternMap,t.InternSet=InternSet,t.Node=Qd,t.Path=Ua,t.Voronoi=qu,t.ZoomTransform=ww,t.active=function(t,n){var e,r,i=t.__transition;if(i)for(r in n=null==n?null:n+"",i)if((e=i[r]).state>qi&&e.name===n)return new po([[t]],Zo,n,+r);return null},t.arc=function(){var t=Cm,n=Pm,e=ym(0),r=null,i=zm,o=$m,a=Dm,u=null,c=km(f);function f(){var f,s,l=+t.apply(this,arguments),h=+n.apply(this,arguments),d=i.apply(this,arguments)-Sm,p=o.apply(this,arguments)-Sm,g=vm(p-d),y=p>d;if(u||(u=f=c()),hTm)if(g>Em-Tm)u.moveTo(h*bm(d),h*wm(d)),u.arc(0,0,h,d,p,!y),l>Tm&&(u.moveTo(l*bm(p),l*wm(p)),u.arc(0,0,l,p,d,y));else{var v,_,b=d,m=p,x=d,w=p,M=g,T=g,A=a.apply(this,arguments)/2,S=A>Tm&&(r?+r.apply(this,arguments):Mm(l*l+h*h)),E=xm(vm(h-l)/2,+e.apply(this,arguments)),N=E,k=E;if(S>Tm){var C=Nm(S/l*wm(A)),P=Nm(S/h*wm(A));(M-=2*C)>Tm?(x+=C*=y?1:-1,w-=C):(M=0,x=w=(d+p)/2),(T-=2*P)>Tm?(b+=P*=y?1:-1,m-=P):(T=0,b=m=(d+p)/2)}var z=h*bm(b),$=h*wm(b),D=l*bm(w),R=l*wm(w);if(E>Tm){var F,q=h*bm(m),U=h*wm(m),I=l*bm(x),O=l*wm(x);if(g1?0:t<-1?Am:Math.acos(t)}((B*L+Y*j)/(Mm(B*B+Y*Y)*Mm(L*L+j*j)))/2),X=Mm(F[0]*F[0]+F[1]*F[1]);N=xm(E,(l-X)/(H-1)),k=xm(E,(h-X)/(H+1))}else N=k=0}T>Tm?k>Tm?(v=Rm(I,O,z,$,h,k,y),_=Rm(q,U,D,R,h,k,y),u.moveTo(v.cx+v.x01,v.cy+v.y01),kTm&&M>Tm?N>Tm?(v=Rm(D,R,q,U,l,-N,y),_=Rm(z,$,I,O,l,-N,y),u.lineTo(v.cx+v.x01,v.cy+v.y01),N=0))throw new RangeError("invalid r");let e=t.length;if(!((e=Math.floor(e))>=0))throw new RangeError("invalid length");if(!e||!n)return t;const r=y(n),i=t.slice();return r(t,i,0,e,1),r(i,t,0,e,1),r(t,i,0,e,1),t},t.blur2=l,t.blurImage=h,t.brush=function(){return wa(la)},t.brushSelection=function(t){var n=t.__brush;return n?n.dim.output(n.selection):null},t.brushX=function(){return wa(fa)},t.brushY=function(){return wa(sa)},t.buffer=function(t,n){return fetch(t,n).then(_c)},t.chord=function(){return za(!1,!1)},t.chordDirected=function(){return za(!0,!1)},t.chordTranspose=function(){return za(!1,!0)},t.cluster=function(){var t=Ld,n=1,e=1,r=!1;function i(i){var o,a=0;i.eachAfter((function(n){var e=n.children;e?(n.x=function(t){return t.reduce(jd,0)/t.length}(e),n.y=function(t){return 1+t.reduce(Hd,0)}(e)):(n.x=o?a+=t(n,o):0,n.y=0,o=n)}));var u=function(t){for(var n;n=t.children;)t=n[0];return t}(i),c=function(t){for(var n;n=t.children;)t=n[n.length-1];return t}(i),f=u.x-t(u,c)/2,s=c.x+t(c,u)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*n,t.y=(i.y-t.y)*e}:function(t){t.x=(t.x-f)/(s-f)*n,t.y=(1-(i.y?t.y/i.y:1))*e})}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.color=ze,t.contourDensity=function(){var t=fu,n=su,e=lu,r=960,i=500,o=20,a=2,u=3*o,c=r+2*u>>a,f=i+2*u>>a,s=Qa(20);function h(r){var i=new Float32Array(c*f),s=Math.pow(2,-a),h=-1;for(const o of r){var d=(t(o,++h,r)+u)*s,p=(n(o,h,r)+u)*s,g=+e(o,h,r);if(g&&d>=0&&d=0&&pt*r)))(n).map(((t,n)=>(t.value=+e[n],p(t))))}function p(t){return t.coordinates.forEach(g),t}function g(t){t.forEach(y)}function y(t){t.forEach(v)}function v(t){t[0]=t[0]*Math.pow(2,a)-u,t[1]=t[1]*Math.pow(2,a)-u}function _(){return c=r+2*(u=3*o)>>a,f=i+2*u>>a,d}return d.contours=function(t){var n=h(t),e=iu().size([c,f]),r=Math.pow(2,2*a),i=t=>{t=+t;var i=p(e.contour(n,t*r));return i.value=t,i};return Object.defineProperty(i,"max",{get:()=>J(n)/r}),i},d.x=function(n){return arguments.length?(t="function"==typeof n?n:Qa(+n),d):t},d.y=function(t){return arguments.length?(n="function"==typeof t?t:Qa(+t),d):n},d.weight=function(t){return arguments.length?(e="function"==typeof t?t:Qa(+t),d):e},d.size=function(t){if(!arguments.length)return[r,i];var n=+t[0],e=+t[1];if(!(n>=0&&e>=0))throw new Error("invalid size");return r=n,i=e,_()},d.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return a=Math.floor(Math.log(t)/Math.LN2),_()},d.thresholds=function(t){return arguments.length?(s="function"==typeof t?t:Array.isArray(t)?Qa(Za.call(t)):Qa(t),d):s},d.bandwidth=function(t){if(!arguments.length)return Math.sqrt(o*(o+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return o=(Math.sqrt(4*t*t+1)-1)/2,_()},d},t.contours=iu,t.count=v,t.create=function(t){return Zn(Yt(t).call(document.documentElement))},t.creator=Yt,t.cross=function(...t){const n="function"==typeof t[t.length-1]&&function(t){return n=>t(...n)}(t.pop()),e=(t=t.map(m)).map(_),r=t.length-1,i=new Array(r+1).fill(0),o=[];if(r<0||e.some(b))return o;for(;;){o.push(i.map(((n,e)=>t[e][n])));let a=r;for(;++i[a]===e[a];){if(0===a)return n?o.map(n):o;i[a--]=0}}},t.csv=wc,t.csvFormat=rc,t.csvFormatBody=ic,t.csvFormatRow=ac,t.csvFormatRows=oc,t.csvFormatValue=uc,t.csvParse=nc,t.csvParseRows=ec,t.cubehelix=Tr,t.cumsum=function(t,n){var e=0,r=0;return Float64Array.from(t,void 0===n?t=>e+=+t||0:i=>e+=+n(i,r++,t)||0)},t.curveBasis=function(t){return new Fx(t)},t.curveBasisClosed=function(t){return new qx(t)},t.curveBasisOpen=function(t){return new Ux(t)},t.curveBumpX=nx,t.curveBumpY=ex,t.curveBundle=Ox,t.curveCardinal=Lx,t.curveCardinalClosed=Hx,t.curveCardinalOpen=Gx,t.curveCatmullRom=Zx,t.curveCatmullRomClosed=Qx,t.curveCatmullRomOpen=tw,t.curveLinear=Im,t.curveLinearClosed=function(t){return new nw(t)},t.curveMonotoneX=function(t){return new aw(t)},t.curveMonotoneY=function(t){return new uw(t)},t.curveNatural=function(t){return new fw(t)},t.curveStep=function(t){return new lw(t,.5)},t.curveStepAfter=function(t){return new lw(t,1)},t.curveStepBefore=function(t){return new lw(t,0)},t.descending=e,t.deviation=w,t.difference=function(t,...n){t=new InternSet(t);for(const e of n)for(const n of e)t.delete(n);return t},t.disjoint=function(t,n){const e=n[Symbol.iterator](),r=new InternSet;for(const n of t){if(r.has(n))return!1;let t,i;for(;({value:t,done:i}=e.next())&&!i;){if(Object.is(n,t))return!1;r.add(t)}}return!0},t.dispatch=$t,t.drag=function(){var t,n,e,r,i=se,o=le,a=he,u=de,c={},f=$t("start","drag","end"),s=0,l=0;function h(t){t.on("mousedown.drag",d).filter(u).on("touchstart.drag",y).on("touchmove.drag",v,ee).on("touchend.drag touchcancel.drag",_).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function d(a,u){if(!r&&i.call(this,a,u)){var c=b(this,o.call(this,a,u),a,u,"mouse");c&&(Zn(a.view).on("mousemove.drag",p,re).on("mouseup.drag",g,re),ae(a.view),ie(a),e=!1,t=a.clientX,n=a.clientY,c("start",a))}}function p(r){if(oe(r),!e){var i=r.clientX-t,o=r.clientY-n;e=i*i+o*o>l}c.mouse("drag",r)}function g(t){Zn(t.view).on("mousemove.drag mouseup.drag",null),ue(t.view,e),oe(t),c.mouse("end",t)}function y(t,n){if(i.call(this,t,n)){var e,r,a=t.changedTouches,u=o.call(this,t,n),c=a.length;for(e=0;e+t,t.easePoly=wo,t.easePolyIn=mo,t.easePolyInOut=wo,t.easePolyOut=xo,t.easeQuad=_o,t.easeQuadIn=function(t){return t*t},t.easeQuadInOut=_o,t.easeQuadOut=function(t){return t*(2-t)},t.easeSin=Ao,t.easeSinIn=function(t){return 1==+t?1:1-Math.cos(t*To)},t.easeSinInOut=Ao,t.easeSinOut=function(t){return Math.sin(t*To)},t.every=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");let e=-1;for(const r of t)if(!n(r,++e,t))return!1;return!0},t.extent=M,t.fcumsum=function(t,n){const e=new T;let r=-1;return Float64Array.from(t,void 0===n?t=>e.add(+t||0):i=>e.add(+n(i,++r,t)||0))},t.filter=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");const e=[];let r=-1;for(const i of t)n(i,++r,t)&&e.push(i);return e},t.flatGroup=function(t,...n){return z(P(t,...n),n)},t.flatRollup=function(t,n,...e){return z(D(t,n,...e),e)},t.forceCenter=function(t,n){var e,r=1;function i(){var i,o,a=e.length,u=0,c=0;for(i=0;if+p||os+p||ac.index){var g=f-u.x-u.vx,y=s-u.y-u.vy,v=g*g+y*y;vt.r&&(t.r=t[n].r)}function c(){if(n){var r,i,o=n.length;for(e=new Array(o),r=0;r[u(t,n,r),t])));for(a=0,i=new Array(f);a=u)){(t.data!==n||t.next)&&(0===l&&(p+=(l=Uc(e))*l),0===h&&(p+=(h=Uc(e))*h),p(t=(Lc*t+jc)%Hc)/Hc}();function l(){h(),f.call("tick",n),e1?(null==e?u.delete(t):u.set(t,p(e)),n):u.get(t)},find:function(n,e,r){var i,o,a,u,c,f=0,s=t.length;for(null==r?r=1/0:r*=r,f=0;f1?(f.on(t,e),n):f.on(t)}}},t.forceX=function(t){var n,e,r,i=qc(.1);function o(t){for(var i,o=0,a=n.length;o=.12&&i<.234&&r>=-.425&&r<-.214?u:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:a).invert(t)},s.stream=function(e){return t&&n===e?t:(r=[a.stream(n=e),u.stream(e),c.stream(e)],i=r.length,t={point:function(t,n){for(var e=-1;++ejs(r[0],r[1])&&(r[1]=i[1]),js(i[0],r[1])>js(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=js(r[1],i[0]))>a&&(a=u,Wf=i[0],Kf=r[1])}return is=os=null,Wf===1/0||Zf===1/0?[[NaN,NaN],[NaN,NaN]]:[[Wf,Zf],[Kf,Qf]]},t.geoCentroid=function(t){ms=xs=ws=Ms=Ts=As=Ss=Es=0,Ns=new T,ks=new T,Cs=new T,Lf(t,Gs);var n=+Ns,e=+ks,r=+Cs,i=Ef(n,e,r);return i=0))throw new RangeError(`invalid digits: ${t}`);i=n}return null===n&&(r=new ed(i)),a},a.projection(t).digits(i).context(n)},t.geoProjection=yd,t.geoProjectionMutator=vd,t.geoRotation=ll,t.geoStereographic=function(){return yd(Bd).scale(250).clipAngle(142)},t.geoStereographicRaw=Bd,t.geoStream=Lf,t.geoTransform=function(t){return{stream:id(t)}},t.geoTransverseMercator=function(){var t=Ed(Yd),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):[(t=n())[1],-t[0]]},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):[(t=e())[0],t[1],t[2]-90]},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=Yd,t.gray=function(t,n){return new ur(t,0,0,null==n?1:n)},t.greatest=ot,t.greatestIndex=function(t,e=n){if(1===e.length)return tt(t,e);let r,i=-1,o=-1;for(const n of t)++o,(i<0?0===e(n,n):e(n,r)>0)&&(r=n,i=o);return i},t.group=C,t.groupSort=function(t,e,r){return(2!==e.length?U($(t,e,r),(([t,e],[r,i])=>n(e,i)||n(t,r))):U(C(t,r),(([t,r],[i,o])=>e(r,o)||n(t,i)))).map((([t])=>t))},t.groups=P,t.hcl=dr,t.hierarchy=Gd,t.histogram=Q,t.hsl=He,t.html=Ec,t.image=function(t,n){return new Promise((function(e,r){var i=new Image;for(var o in n)i[o]=n[o];i.onerror=r,i.onload=function(){e(i)},i.src=t}))},t.index=function(t,...n){return F(t,k,R,n)},t.indexes=function(t,...n){return F(t,Array.from,R,n)},t.interpolate=Gr,t.interpolateArray=function(t,n){return(Ir(n)?Ur:Or)(t,n)},t.interpolateBasis=Er,t.interpolateBasisClosed=Nr,t.interpolateBlues=Gb,t.interpolateBrBG=ob,t.interpolateBuGn=Mb,t.interpolateBuPu=Ab,t.interpolateCividis=function(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(-4.54-t*(35.34-t*(2381.73-t*(6402.7-t*(7024.72-2710.57*t)))))))+", "+Math.max(0,Math.min(255,Math.round(32.49+t*(170.73+t*(52.82-t*(131.46-t*(176.58-67.37*t)))))))+", "+Math.max(0,Math.min(255,Math.round(81.24+t*(442.36-t*(2482.43-t*(6167.24-t*(6614.94-2475.67*t)))))))+")"},t.interpolateCool=am,t.interpolateCubehelix=li,t.interpolateCubehelixDefault=im,t.interpolateCubehelixLong=hi,t.interpolateDate=Br,t.interpolateDiscrete=function(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}},t.interpolateGnBu=Eb,t.interpolateGreens=Wb,t.interpolateGreys=Kb,t.interpolateHcl=ci,t.interpolateHclLong=fi,t.interpolateHsl=oi,t.interpolateHslLong=ai,t.interpolateHue=function(t,n){var e=Pr(+t,+n);return function(t){var n=e(t);return n-360*Math.floor(n/360)}},t.interpolateInferno=pm,t.interpolateLab=function(t,n){var e=$r((t=ar(t)).l,(n=ar(n)).l),r=$r(t.a,n.a),i=$r(t.b,n.b),o=$r(t.opacity,n.opacity);return function(n){return t.l=e(n),t.a=r(n),t.b=i(n),t.opacity=o(n),t+""}},t.interpolateMagma=dm,t.interpolateNumber=Yr,t.interpolateNumberArray=Ur,t.interpolateObject=Lr,t.interpolateOrRd=kb,t.interpolateOranges=rm,t.interpolatePRGn=ub,t.interpolatePiYG=fb,t.interpolatePlasma=gm,t.interpolatePuBu=$b,t.interpolatePuBuGn=Pb,t.interpolatePuOr=lb,t.interpolatePuRd=Rb,t.interpolatePurples=Jb,t.interpolateRainbow=function(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return um.h=360*t-100,um.s=1.5-1.5*n,um.l=.8-.9*n,um+""},t.interpolateRdBu=db,t.interpolateRdGy=gb,t.interpolateRdPu=qb,t.interpolateRdYlBu=vb,t.interpolateRdYlGn=bb,t.interpolateReds=nm,t.interpolateRgb=Dr,t.interpolateRgbBasis=Fr,t.interpolateRgbBasisClosed=qr,t.interpolateRound=Vr,t.interpolateSinebow=function(t){var n;return t=(.5-t)*Math.PI,cm.r=255*(n=Math.sin(t))*n,cm.g=255*(n=Math.sin(t+fm))*n,cm.b=255*(n=Math.sin(t+sm))*n,cm+""},t.interpolateSpectral=xb,t.interpolateString=Xr,t.interpolateTransformCss=ti,t.interpolateTransformSvg=ni,t.interpolateTurbo=function(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(34.61+t*(1172.33-t*(10793.56-t*(33300.12-t*(38394.49-14825.05*t)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+t*(557.33+t*(1225.33-t*(3574.96-t*(1073.77+707.56*t)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+t*(3211.1-t*(15327.97-t*(27814-t*(22569.18-6838.66*t)))))))+")"},t.interpolateViridis=hm,t.interpolateWarm=om,t.interpolateYlGn=Bb,t.interpolateYlGnBu=Ib,t.interpolateYlOrBr=Lb,t.interpolateYlOrRd=Hb,t.interpolateZoom=ri,t.interrupt=Gi,t.intersection=function(t,...n){t=new InternSet(t),n=n.map(vt);t:for(const e of t)for(const r of n)if(!r.has(e)){t.delete(e);continue t}return t},t.interval=function(t,n,e){var r=new Ei,i=n;return null==n?(r.restart(t,n,e),r):(r._restart=r.restart,r.restart=function(t,n,e){n=+n,e=null==e?Ai():+e,r._restart((function o(a){a+=i,r._restart(o,i+=n,e),t(a)}),n,e)},r.restart(t,n,e),r)},t.isoFormat=D_,t.isoParse=F_,t.json=function(t,n){return fetch(t,n).then(Tc)},t.lab=ar,t.lch=function(t,n,e,r){return 1===arguments.length?hr(t):new pr(e,n,t,null==r?1:r)},t.least=function(t,e=n){let r,i=!1;if(1===e.length){let o;for(const a of t){const t=e(a);(i?n(t,o)<0:0===n(t,t))&&(r=a,o=t,i=!0)}}else for(const n of t)(i?e(n,r)<0:0===e(n,n))&&(r=n,i=!0);return r},t.leastIndex=ht,t.line=Ym,t.lineRadial=Zm,t.link=ax,t.linkHorizontal=function(){return ax(nx)},t.linkRadial=function(){const t=ax(rx);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.linkVertical=function(){return ax(ex)},t.local=Qn,t.map=function(t,n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");if("function"!=typeof n)throw new TypeError("mapper is not a function");return Array.from(t,((e,r)=>n(e,r,t)))},t.matcher=Vt,t.max=J,t.maxIndex=tt,t.mean=function(t,n){let e=0,r=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(++e,r+=n);else{let i=-1;for(let o of t)null!=(o=n(o,++i,t))&&(o=+o)>=o&&(++e,r+=o)}if(e)return r/e},t.median=function(t,n){return at(t,.5,n)},t.medianIndex=function(t,n){return ct(t,.5,n)},t.merge=ft,t.min=nt,t.minIndex=et,t.mode=function(t,n){const e=new InternMap;if(void 0===n)for(let n of t)null!=n&&n>=n&&e.set(n,(e.get(n)||0)+1);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&i>=i&&e.set(i,(e.get(i)||0)+1)}let r,i=0;for(const[t,n]of e)n>i&&(i=n,r=t);return r},t.namespace=It,t.namespaces=Ut,t.nice=Z,t.now=Ai,t.pack=function(){var t=null,n=1,e=1,r=np;function i(i){const o=ap();return i.x=n/2,i.y=e/2,t?i.eachBefore(xp(t)).eachAfter(wp(r,.5,o)).eachBefore(Mp(1)):i.eachBefore(xp(mp)).eachAfter(wp(np,1,o)).eachAfter(wp(r,i.r/Math.min(n,e),o)).eachBefore(Mp(Math.min(n,e)/(2*i.r))),i}return i.radius=function(n){return arguments.length?(t=Jd(n),i):t},i.size=function(t){return arguments.length?(n=+t[0],e=+t[1],i):[n,e]},i.padding=function(t){return arguments.length?(r="function"==typeof t?t:ep(+t),i):r},i},t.packEnclose=function(t){return up(t,ap())},t.packSiblings=function(t){return bp(t,ap()),t},t.pairs=function(t,n=st){const e=[];let r,i=!1;for(const o of t)i&&e.push(n(r,o)),r=o,i=!0;return e},t.partition=function(){var t=1,n=1,e=0,r=!1;function i(i){var o=i.height+1;return i.x0=i.y0=e,i.x1=t,i.y1=n/o,i.eachBefore(function(t,n){return function(r){r.children&&Ap(r,r.x0,t*(r.depth+1)/n,r.x1,t*(r.depth+2)/n);var i=r.x0,o=r.y0,a=r.x1-e,u=r.y1-e;a0&&(d+=l);for(null!=n?p.sort((function(t,e){return n(g[t],g[e])})):null!=e&&p.sort((function(t,n){return e(a[t],a[n])})),u=0,f=d?(v-h*b)/d:0;u0?l*f:0)+b,g[c]={data:a[c],index:u,value:l,startAngle:y,endAngle:s,padAngle:_};return g}return a.value=function(n){return arguments.length?(t="function"==typeof n?n:ym(+n),a):t},a.sortValues=function(t){return arguments.length?(n=t,e=null,a):n},a.sort=function(t){return arguments.length?(e=t,n=null,a):e},a.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:ym(+t),a):r},a.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:ym(+t),a):i},a.padAngle=function(t){return arguments.length?(o="function"==typeof t?t:ym(+t),a):o},a},t.piecewise=di,t.pointRadial=Qm,t.pointer=ne,t.pointers=function(t,n){return t.target&&(t=te(t),void 0===n&&(n=t.currentTarget),t=t.touches||[t]),Array.from(t,(t=>ne(t,n)))},t.polygonArea=function(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++eu!=f>u&&a<(c-e)*(u-r)/(f-r)+e&&(s=!s),c=e,f=r;return s},t.polygonHull=function(t){if((e=t.length)<3)return null;var n,e,r=new Array(e),i=new Array(e);for(n=0;n=0;--n)f.push(t[r[o[n]][2]]);for(n=+u;n(n=1664525*n+1013904223|0,lg*(n>>>0))},t.randomLogNormal=Kp,t.randomLogistic=fg,t.randomNormal=Zp,t.randomPareto=ng,t.randomPoisson=sg,t.randomUniform=Vp,t.randomWeibull=ug,t.range=lt,t.rank=function(t,e=n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");let r=Array.from(t);const i=new Float64Array(r.length);2!==e.length&&(r=r.map(e),e=n);const o=(t,n)=>e(r[t],r[n]);let a,u;return(t=Uint32Array.from(r,((t,n)=>n))).sort(e===n?(t,n)=>O(r[t],r[n]):I(o)),t.forEach(((t,n)=>{const e=o(t,void 0===a?t:a);e>=0?((void 0===a||e>0)&&(a=t,u=n),i[t]=u):i[t]=NaN})),i},t.reduce=function(t,n,e){if("function"!=typeof n)throw new TypeError("reducer is not a function");const r=t[Symbol.iterator]();let i,o,a=-1;if(arguments.length<3){if(({done:i,value:e}=r.next()),i)return;++a}for(;({done:i,value:o}=r.next()),!i;)e=n(e,o,++a,t);return e},t.reverse=function(t){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");return Array.from(t).reverse()},t.rgb=Fe,t.ribbon=function(){return Wa()},t.ribbonArrow=function(){return Wa(Va)},t.rollup=$,t.rollups=D,t.scaleBand=yg,t.scaleDiverging=function t(){var n=Ng(L_()(mg));return n.copy=function(){return B_(n,t())},dg.apply(n,arguments)},t.scaleDivergingLog=function t(){var n=Fg(L_()).domain([.1,1,10]);return n.copy=function(){return B_(n,t()).base(n.base())},dg.apply(n,arguments)},t.scaleDivergingPow=j_,t.scaleDivergingSqrt=function(){return j_.apply(null,arguments).exponent(.5)},t.scaleDivergingSymlog=function t(){var n=Ig(L_());return n.copy=function(){return B_(n,t()).constant(n.constant())},dg.apply(n,arguments)},t.scaleIdentity=function t(n){var e;function r(t){return null==t||isNaN(t=+t)?e:t}return r.invert=r,r.domain=r.range=function(t){return arguments.length?(n=Array.from(t,_g),r):n.slice()},r.unknown=function(t){return arguments.length?(e=t,r):e},r.copy=function(){return t(n).unknown(e)},n=arguments.length?Array.from(n,_g):[0,1],Ng(r)},t.scaleImplicit=pg,t.scaleLinear=function t(){var n=Sg();return n.copy=function(){return Tg(n,t())},hg.apply(n,arguments),Ng(n)},t.scaleLog=function t(){const n=Fg(Ag()).domain([1,10]);return n.copy=()=>Tg(n,t()).base(n.base()),hg.apply(n,arguments),n},t.scaleOrdinal=gg,t.scalePoint=function(){return vg(yg.apply(null,arguments).paddingInner(1))},t.scalePow=jg,t.scaleQuantile=function t(){var e,r=[],i=[],o=[];function a(){var t=0,n=Math.max(1,i.length);for(o=new Array(n-1);++t0?o[n-1]:r[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},u.unknown=function(t){return arguments.length?(n=t,u):u},u.thresholds=function(){return o.slice()},u.copy=function(){return t().domain([e,r]).range(a).unknown(n)},hg.apply(Ng(u),arguments)},t.scaleRadial=function t(){var n,e=Sg(),r=[0,1],i=!1;function o(t){var r=function(t){return Math.sign(t)*Math.sqrt(Math.abs(t))}(e(t));return isNaN(r)?n:i?Math.round(r):r}return o.invert=function(t){return e.invert(Hg(t))},o.domain=function(t){return arguments.length?(e.domain(t),o):e.domain()},o.range=function(t){return arguments.length?(e.range((r=Array.from(t,_g)).map(Hg)),o):r.slice()},o.rangeRound=function(t){return o.range(t).round(!0)},o.round=function(t){return arguments.length?(i=!!t,o):i},o.clamp=function(t){return arguments.length?(e.clamp(t),o):e.clamp()},o.unknown=function(t){return arguments.length?(n=t,o):n},o.copy=function(){return t(e.domain(),r).round(i).clamp(e.clamp()).unknown(n)},hg.apply(o,arguments),Ng(o)},t.scaleSequential=function t(){var n=Ng(O_()(mg));return n.copy=function(){return B_(n,t())},dg.apply(n,arguments)},t.scaleSequentialLog=function t(){var n=Fg(O_()).domain([1,10]);return n.copy=function(){return B_(n,t()).base(n.base())},dg.apply(n,arguments)},t.scaleSequentialPow=Y_,t.scaleSequentialQuantile=function t(){var e=[],r=mg;function i(t){if(null!=t&&!isNaN(t=+t))return r((s(e,t,1)-1)/(e.length-1))}return i.domain=function(t){if(!arguments.length)return e.slice();e=[];for(let n of t)null==n||isNaN(n=+n)||e.push(n);return e.sort(n),i},i.interpolator=function(t){return arguments.length?(r=t,i):r},i.range=function(){return e.map(((t,n)=>r(n/(e.length-1))))},i.quantiles=function(t){return Array.from({length:t+1},((n,r)=>at(e,r/t)))},i.copy=function(){return t(r).domain(e)},dg.apply(i,arguments)},t.scaleSequentialSqrt=function(){return Y_.apply(null,arguments).exponent(.5)},t.scaleSequentialSymlog=function t(){var n=Ig(O_());return n.copy=function(){return B_(n,t()).constant(n.constant())},dg.apply(n,arguments)},t.scaleSqrt=function(){return jg.apply(null,arguments).exponent(.5)},t.scaleSymlog=function t(){var n=Ig(Ag());return n.copy=function(){return Tg(n,t()).constant(n.constant())},hg.apply(n,arguments)},t.scaleThreshold=function t(){var n,e=[.5],r=[0,1],i=1;function o(t){return null!=t&&t<=t?r[s(e,t,0,i)]:n}return o.domain=function(t){return arguments.length?(e=Array.from(t),i=Math.min(e.length,r.length-1),o):e.slice()},o.range=function(t){return arguments.length?(r=Array.from(t),i=Math.min(e.length,r.length-1),o):r.slice()},o.invertExtent=function(t){var n=r.indexOf(t);return[e[n-1],e[n]]},o.unknown=function(t){return arguments.length?(n=t,o):n},o.copy=function(){return t().domain(e).range(r).unknown(n)},hg.apply(o,arguments)},t.scaleTime=function(){return hg.apply(I_(uv,cv,tv,Zy,xy,py,sy,ay,iy,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)]),arguments)},t.scaleUtc=function(){return hg.apply(I_(ov,av,ev,Qy,Fy,yy,hy,cy,iy,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)]),arguments)},t.scan=function(t,n){const e=ht(t,n);return e<0?void 0:e},t.schemeAccent=G_,t.schemeBlues=Xb,t.schemeBrBG=ib,t.schemeBuGn=wb,t.schemeBuPu=Tb,t.schemeCategory10=X_,t.schemeDark2=V_,t.schemeGnBu=Sb,t.schemeGreens=Vb,t.schemeGreys=Zb,t.schemeObservable10=W_,t.schemeOrRd=Nb,t.schemeOranges=em,t.schemePRGn=ab,t.schemePaired=Z_,t.schemePastel1=K_,t.schemePastel2=Q_,t.schemePiYG=cb,t.schemePuBu=zb,t.schemePuBuGn=Cb,t.schemePuOr=sb,t.schemePuRd=Db,t.schemePurples=Qb,t.schemeRdBu=hb,t.schemeRdGy=pb,t.schemeRdPu=Fb,t.schemeRdYlBu=yb,t.schemeRdYlGn=_b,t.schemeReds=tm,t.schemeSet1=J_,t.schemeSet2=tb,t.schemeSet3=nb,t.schemeSpectral=mb,t.schemeTableau10=eb,t.schemeYlGn=Ob,t.schemeYlGnBu=Ub,t.schemeYlOrBr=Yb,t.schemeYlOrRd=jb,t.select=Zn,t.selectAll=function(t){return"string"==typeof t?new Vn([document.querySelectorAll(t)],[document.documentElement]):new Vn([Ht(t)],Gn)},t.selection=Wn,t.selector=jt,t.selectorAll=Gt,t.shuffle=dt,t.shuffler=pt,t.some=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");let e=-1;for(const r of t)if(n(r,++e,t))return!0;return!1},t.sort=U,t.stack=function(){var t=ym([]),n=dw,e=hw,r=pw;function i(i){var o,a,u=Array.from(t.apply(this,arguments),gw),c=u.length,f=-1;for(const t of i)for(o=0,++f;o0)for(var e,r,i,o,a,u,c=0,f=t[n[0]].length;c0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):(r[0]=0,r[1]=i)},t.stackOffsetExpand=function(t,n){if((r=t.length)>0){for(var e,r,i,o=0,a=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;afunction(t){t=`${t}`;let n=t.length;zp(t,n-1)&&!zp(t,n-2)&&(t=t.slice(0,-1));return"/"===t[0]?t:`/${t}`}(t(n,e,r)))),e=n.map(Pp),i=new Set(n).add("");for(const t of e)i.has(t)||(i.add(t),n.push(t),e.push(Pp(t)),h.push(Np));d=(t,e)=>n[e],p=(t,n)=>e[n]}for(a=0,i=h.length;a=0&&(f=h[t]).data===Np;--t)f.data=null}if(u.parent=Sp,u.eachBefore((function(t){t.depth=t.parent.depth+1,--i})).eachBefore(Kd),u.parent=null,i>0)throw new Error("cycle");return u}return r.id=function(t){return arguments.length?(n=Jd(t),r):n},r.parentId=function(t){return arguments.length?(e=Jd(t),r):e},r.path=function(n){return arguments.length?(t=Jd(n),r):t},r},t.style=_n,t.subset=function(t,n){return _t(n,t)},t.sum=function(t,n){let e=0;if(void 0===n)for(let n of t)(n=+n)&&(e+=n);else{let r=-1;for(let i of t)(i=+n(i,++r,t))&&(e+=i)}return e},t.superset=_t,t.svg=Nc,t.symbol=function(t,n){let e=null,r=km(i);function i(){let i;if(e||(e=i=r()),t.apply(this,arguments).draw(e,+n.apply(this,arguments)),i)return e=null,i+""||null}return t="function"==typeof t?t:ym(t||fx),n="function"==typeof n?n:ym(void 0===n?64:+n),i.type=function(n){return arguments.length?(t="function"==typeof n?n:ym(n),i):t},i.size=function(t){return arguments.length?(n="function"==typeof t?t:ym(+t),i):n},i.context=function(t){return arguments.length?(e=null==t?null:t,i):e},i},t.symbolAsterisk=cx,t.symbolCircle=fx,t.symbolCross=sx,t.symbolDiamond=dx,t.symbolDiamond2=px,t.symbolPlus=gx,t.symbolSquare=yx,t.symbolSquare2=vx,t.symbolStar=xx,t.symbolTimes=Px,t.symbolTriangle=Mx,t.symbolTriangle2=Ax,t.symbolWye=Cx,t.symbolX=Px,t.symbols=zx,t.symbolsFill=zx,t.symbolsStroke=$x,t.text=mc,t.thresholdFreedmanDiaconis=function(t,n,e){const r=v(t),i=at(t,.75)-at(t,.25);return r&&i?Math.ceil((e-n)/(2*i*Math.pow(r,-1/3))):1},t.thresholdScott=function(t,n,e){const r=v(t),i=w(t);return r&&i?Math.ceil((e-n)*Math.cbrt(r)/(3.49*i)):1},t.thresholdSturges=K,t.tickFormat=Eg,t.tickIncrement=V,t.tickStep=W,t.ticks=G,t.timeDay=py,t.timeDays=gy,t.timeFormatDefaultLocale=P_,t.timeFormatLocale=hv,t.timeFriday=Sy,t.timeFridays=$y,t.timeHour=sy,t.timeHours=ly,t.timeInterval=Vg,t.timeMillisecond=Wg,t.timeMilliseconds=Zg,t.timeMinute=ay,t.timeMinutes=uy,t.timeMonday=wy,t.timeMondays=ky,t.timeMonth=Zy,t.timeMonths=Ky,t.timeSaturday=Ey,t.timeSaturdays=Dy,t.timeSecond=iy,t.timeSeconds=oy,t.timeSunday=xy,t.timeSundays=Ny,t.timeThursday=Ay,t.timeThursdays=zy,t.timeTickInterval=cv,t.timeTicks=uv,t.timeTuesday=My,t.timeTuesdays=Cy,t.timeWednesday=Ty,t.timeWednesdays=Py,t.timeWeek=xy,t.timeWeeks=Ny,t.timeYear=tv,t.timeYears=nv,t.timeout=$i,t.timer=Ni,t.timerFlush=ki,t.transition=go,t.transpose=gt,t.tree=function(){var t=$p,n=1,e=1,r=null;function i(i){var c=function(t){for(var n,e,r,i,o,a=new Up(t,0),u=[a];n=u.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)u.push(e=n.children[i]=new Up(r[i],i)),e.parent=n;return(a.parent=new Up(null,0)).children=[a],a}(i);if(c.eachAfter(o),c.parent.m=-c.z,c.eachBefore(a),r)i.eachBefore(u);else{var f=i,s=i,l=i;i.eachBefore((function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)}));var h=f===s?1:t(f,s)/2,d=h-f.x,p=n/(s.x+h+d),g=e/(l.depth||1);i.eachBefore((function(t){t.x=(t.x+d)*p,t.y=t.depth*g}))}return i}function o(n){var e=n.children,r=n.parent.children,i=n.i?r[n.i-1]:null;if(e){!function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)}(n);var o=(e[0].z+e[e.length-1].z)/2;i?(n.z=i.z+t(n._,i._),n.m=n.z-o):n.z=o}else i&&(n.z=i.z+t(n._,i._));n.parent.A=function(n,e,r){if(e){for(var i,o=n,a=n,u=e,c=o.parent.children[0],f=o.m,s=a.m,l=u.m,h=c.m;u=Rp(u),o=Dp(o),u&&o;)c=Dp(c),(a=Rp(a)).a=n,(i=u.z+l-o.z-f+t(u._,o._))>0&&(Fp(qp(u,n,r),n,i),f+=i,s+=i),l+=u.m,f+=o.m,h+=c.m,s+=a.m;u&&!Rp(a)&&(a.t=u,a.m+=l-s),o&&!Dp(c)&&(c.t=o,c.m+=f-h,r=n)}return r}(n,i,n.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function u(t){t.x*=n,t.y=t.depth*e}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.treemap=function(){var t=Yp,n=!1,e=1,r=1,i=[0],o=np,a=np,u=np,c=np,f=np;function s(t){return t.x0=t.y0=0,t.x1=e,t.y1=r,t.eachBefore(l),i=[0],n&&t.eachBefore(Tp),t}function l(n){var e=i[n.depth],r=n.x0+e,s=n.y0+e,l=n.x1-e,h=n.y1-e;l=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}var l=f[n],h=r/2+l,d=n+1,p=e-1;for(;d>>1;f[g]c-o){var _=r?(i*v+a*y)/r:a;t(n,d,y,i,o,_,c),t(d,e,v,_,o,a,c)}else{var b=r?(o*v+c*y)/r:c;t(n,d,y,i,o,a,b),t(d,e,v,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Ap,t.treemapResquarify=Lp,t.treemapSlice=Ip,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Ip:Ap)(t,n,e,r,i)},t.treemapSquarify=Yp,t.tsv=Mc,t.tsvFormat=lc,t.tsvFormatBody=hc,t.tsvFormatRow=pc,t.tsvFormatRows=dc,t.tsvFormatValue=gc,t.tsvParse=fc,t.tsvParseRows=sc,t.union=function(...t){const n=new InternSet;for(const e of t)for(const t of e)n.add(t);return n},t.unixDay=_y,t.unixDays=by,t.utcDay=yy,t.utcDays=vy,t.utcFriday=By,t.utcFridays=Vy,t.utcHour=hy,t.utcHours=dy,t.utcMillisecond=Wg,t.utcMilliseconds=Zg,t.utcMinute=cy,t.utcMinutes=fy,t.utcMonday=qy,t.utcMondays=jy,t.utcMonth=Qy,t.utcMonths=Jy,t.utcSaturday=Yy,t.utcSaturdays=Wy,t.utcSecond=iy,t.utcSeconds=oy,t.utcSunday=Fy,t.utcSundays=Ly,t.utcThursday=Oy,t.utcThursdays=Gy,t.utcTickInterval=av,t.utcTicks=ov,t.utcTuesday=Uy,t.utcTuesdays=Hy,t.utcWednesday=Iy,t.utcWednesdays=Xy,t.utcWeek=Fy,t.utcWeeks=Ly,t.utcYear=ev,t.utcYears=rv,t.variance=x,t.version="7.9.0",t.window=pn,t.xml=Sc,t.zip=function(){return gt(arguments)},t.zoom=function(){var t,n,e,r=Ew,i=Nw,o=zw,a=Cw,u=Pw,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=ri,h=$t("start","zoom","end"),d=500,p=150,g=0,y=10;function v(t){t.property("__zoom",kw).on("wheel.zoom",T,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",S).filter(u).on("touchstart.zoom",E).on("touchmove.zoom",N).on("touchend.zoom touchcancel.zoom",k).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new ww(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new ww(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e,r){t.on("start.zoom",(function(){w(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){w(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=w(t,o).event(r),u=i.apply(t,o),c=null==e?m(u):"function"==typeof e?e.apply(t,o):e,f=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),s=t.__zoom,h="function"==typeof n?n.apply(t,o):n,d=l(s.invert(c).concat(f/s.k),h.invert(c).concat(f/h.k));return function(t){if(1===t)t=h;else{var n=d(t),e=f/n[2];t=new ww(e,c[0]-n[0]*e,c[1]-n[1]*e)}a.zoom(null,t)}}))}function w(t,n,e){return!e&&t.__zooming||new M(t,n)}function M(t,n){this.that=t,this.args=n,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,n),this.taps=0}function T(t,...n){if(r.apply(this,arguments)){var e=w(this,n).event(t),i=this.__zoom,u=Math.max(c[0],Math.min(c[1],i.k*Math.pow(2,a.apply(this,arguments)))),s=ne(t);if(e.wheel)e.mouse[0][0]===s[0]&&e.mouse[0][1]===s[1]||(e.mouse[1]=i.invert(e.mouse[0]=s)),clearTimeout(e.wheel);else{if(i.k===u)return;e.mouse=[s,i.invert(s)],Gi(this),e.start()}Sw(t),e.wheel=setTimeout((function(){e.wheel=null,e.end()}),p),e.zoom("mouse",o(b(_(i,u),e.mouse[0],e.mouse[1]),e.extent,f))}}function A(t,...n){if(!e&&r.apply(this,arguments)){var i=t.currentTarget,a=w(this,n,!0).event(t),u=Zn(t.view).on("mousemove.zoom",(function(t){if(Sw(t),!a.moved){var n=t.clientX-s,e=t.clientY-l;a.moved=n*n+e*e>g}a.event(t).zoom("mouse",o(b(a.that.__zoom,a.mouse[0]=ne(t,i),a.mouse[1]),a.extent,f))}),!0).on("mouseup.zoom",(function(t){u.on("mousemove.zoom mouseup.zoom",null),ue(t.view,a.moved),Sw(t),a.event(t).end()}),!0),c=ne(t,i),s=t.clientX,l=t.clientY;ae(t.view),Aw(t),a.mouse=[c,this.__zoom.invert(c)],Gi(this),a.start()}}function S(t,...n){if(r.apply(this,arguments)){var e=this.__zoom,a=ne(t.changedTouches?t.changedTouches[0]:t,this),u=e.invert(a),c=e.k*(t.shiftKey?.5:2),l=o(b(_(e,c),a,u),i.apply(this,n),f);Sw(t),s>0?Zn(this).transition().duration(s).call(x,l,a,t):Zn(this).call(v.transform,l,a,t)}}function E(e,...i){if(r.apply(this,arguments)){var o,a,u,c,f=e.touches,s=f.length,l=w(this,i,e.changedTouches.length===s).event(e);for(Aw(e),a=0;a + + + + + + + Speedometer Visualization + + + +
+ + + diff --git a/_examples/recovery_mode_cache/main.go b/_examples/recovery_mode_cache/main.go new file mode 100644 index 00000000..512658b2 --- /dev/null +++ b/_examples/recovery_mode_cache/main.go @@ -0,0 +1,166 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + _ "net/http/pprof" + + "github.com/centrifugal/centrifuge" +) + +var port = flag.Int("port", 8000, "Port to bind app to") + +func handleLog(e centrifuge.LogEntry) { + log.Printf("%s: %v", e.Message, e.Fields) +} + +func authMiddleware(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + newCtx := centrifuge.SetCredentials(ctx, ¢rifuge.Credentials{ + UserID: "", + }) + r = r.WithContext(newCtx) + h.ServeHTTP(w, r) + }) +} + +func waitExitSignal(n *centrifuge.Node, s *http.Server) { + sigCh := make(chan os.Signal, 1) + done := make(chan bool, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigCh + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = n.Shutdown(ctx) + _ = s.Shutdown(ctx) + done <- true + }() + <-done +} + +const exampleChannel = "speed" + +// Check whether channel is allowed for subscribing. In real case permission +// check will probably be more complex than in this example. +func channelSubscribeAllowed(channel string) bool { + return channel == exampleChannel +} + +func main() { + node, _ := centrifuge.New(centrifuge.Config{ + LogLevel: centrifuge.LogLevelInfo, + LogHandler: handleLog, + HistoryMetaTTL: 24 * time.Hour, + }) + + node.OnConnect(func(client *centrifuge.Client) { + transport := client.Transport() + log.Printf("[user %s] connected via %s with protocol: %s", client.UserID(), transport.Name(), transport.Protocol()) + + client.OnSubscribe(func(e centrifuge.SubscribeEvent, cb centrifuge.SubscribeCallback) { + log.Printf("[user %s] subscribes on %s", client.UserID(), e.Channel) + + if !channelSubscribeAllowed(e.Channel) { + cb(centrifuge.SubscribeReply{}, centrifuge.ErrorPermissionDenied) + return + } + + cb(centrifuge.SubscribeReply{ + Options: centrifuge.SubscribeOptions{ + EnableRecovery: true, + RecoveryMode: centrifuge.RecoveryModeCache, + }, + }, nil) + }) + + client.OnUnsubscribe(func(e centrifuge.UnsubscribeEvent) { + log.Printf("[user %s] unsubscribed from %s: %s", client.UserID(), e.Channel, e.Reason) + }) + + client.OnDisconnect(func(e centrifuge.DisconnectEvent) { + log.Printf("[user %s] disconnected: %s", client.UserID(), e.Reason) + }) + }) + + if err := node.Run(); err != nil { + log.Fatal(err) + } + + go func() { + const ( + accelerationRate = 2.0 // Speed increment per 100 ms + brakingRate = 10.0 // Speed decrement per 100 ms + maxSpeed = 190.0 + minSpeed = 50.0 + ) + + speed := 0.0 + increasing := true + + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if increasing { + speed += accelerationRate + if speed >= maxSpeed { + increasing = false + } + } else { + speed -= brakingRate + if speed <= minSpeed { + increasing = true + } + } + _, err := node.Publish( + exampleChannel, + []byte(`{"speed": `+fmt.Sprint(speed)+`}`), + centrifuge.WithHistory(1, time.Minute), + ) + if err != nil { + log.Printf("error publishing to personal channel: %s", err) + } + } + } + }() + + mux := http.DefaultServeMux + + websocketHandler := centrifuge.NewWebsocketHandler(node, centrifuge.WebsocketConfig{ + ReadBufferSize: 1024, + UseWriteBufferPool: true, + }) + mux.Handle("/connection/websocket", authMiddleware(websocketHandler)) + mux.Handle("/", http.FileServer(http.Dir("./"))) + + server := &http.Server{ + Handler: mux, + Addr: "127.0.0.1:" + strconv.Itoa(*port), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + + log.Print("Starting server, visit http://localhost:8000") + go func() { + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatal(err) + } + }() + + waitExitSignal(node, server) + log.Println("bye!") +} diff --git a/_examples/recovery_mode_cache/readme.md b/_examples/recovery_mode_cache/readme.md new file mode 100644 index 00000000..3f2912df --- /dev/null +++ b/_examples/recovery_mode_cache/readme.md @@ -0,0 +1,7 @@ +Demonstration on using cache recovery mode. + +``` +go run main.go +``` + +Then go to http://localhost:8000 diff --git a/broker.go b/broker.go index 1f40f4c4..348d6ebe 100644 --- a/broker.go +++ b/broker.go @@ -35,7 +35,7 @@ type ClientInfo struct { // BrokerEventHandler can handle messages received from PUB/SUB system. type BrokerEventHandler interface { // HandlePublication to handle received Publications. - HandlePublication(ch string, pub *Publication, sp StreamPosition) error + HandlePublication(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error // HandleJoin to handle received Join messages. HandleJoin(ch string, info *ClientInfo) error // HandleLeave to handle received Leave messages. @@ -112,6 +112,8 @@ type PublishOptions struct { // with second precision, so don't set something less than one second here. By default, // Centrifuge uses 5 minutes as idempotent result TTL. IdempotentResultTTL time.Duration + // UseDelta enables using delta encoding for the publication. + UseDelta bool } // Broker is responsible for PUB/SUB mechanics. diff --git a/broker_memory.go b/broker_memory.go index d2f01625..f2097ea5 100644 --- a/broker_memory.go +++ b/broker_memory.go @@ -3,6 +3,7 @@ package centrifuge import ( "container/heap" "context" + "fmt" "sync" "time" @@ -104,8 +105,11 @@ func (b *MemoryBroker) Publish(ch string, data []byte, opts PublishOptions) (Str Info: opts.ClientInfo, Tags: opts.Tags, } + var prevPub *Publication if opts.HistorySize > 0 && opts.HistoryTTL > 0 { - streamTop, err := b.historyHub.add(ch, pub, opts) + var err error + var streamTop StreamPosition + streamTop, prevPub, err = b.historyHub.add(ch, pub, opts) if err != nil { return StreamPosition{}, false, err } @@ -117,7 +121,7 @@ func (b *MemoryBroker) Publish(ch string, data []byte, opts PublishOptions) (Str } b.saveResultToCache(ch, opts.IdempotencyKey, streamTop, resultExpireSeconds) } - return streamTop, false, b.eventHandler.HandlePublication(ch, pub, streamTop) + return streamTop, false, b.eventHandler.HandlePublication(ch, pub, streamTop, prevPub) } streamPosition := StreamPosition{} if opts.IdempotencyKey != "" { @@ -127,7 +131,7 @@ func (b *MemoryBroker) Publish(ch string, data []byte, opts PublishOptions) (Str } b.saveResultToCache(ch, opts.IdempotencyKey, streamPosition, resultExpireSeconds) } - return streamPosition, false, b.eventHandler.HandlePublication(ch, pub, StreamPosition{}) + return streamPosition, false, b.eventHandler.HandlePublication(ch, pub, StreamPosition{}, prevPub) } func (b *MemoryBroker) getResultFromCache(ch string, key string) (StreamPosition, bool) { @@ -239,6 +243,10 @@ func newHistoryHub(historyMetaTTL time.Duration, closeCh chan struct{}) *history } } +func (h *historyHub) close() { + close(h.closeCh) +} + func (h *historyHub) runCleanups() { go h.expireStreams() go h.removeStreams() @@ -324,10 +332,24 @@ func (h *historyHub) expireStreams() { } } -func (h *historyHub) add(ch string, pub *Publication, opts PublishOptions) (StreamPosition, error) { +func (h *historyHub) add(ch string, pub *Publication, opts PublishOptions) (StreamPosition, *Publication, error) { h.Lock() defer h.Unlock() + var prevPub *Publication // May be nil is there were no previous publications. + if opts.UseDelta { + pubs, _, err := h.getLocked(ch, HistoryOptions{Filter: HistoryFilter{ + Limit: 1, + Reverse: true, + }, MetaTTL: opts.HistoryMetaTTL}) + if err != nil { + return StreamPosition{}, nil, fmt.Errorf("error getting previous publication from stream: %w", err) + } + if len(pubs) > 0 { + prevPub = pubs[0] + } + } + var offset uint64 var epoch string @@ -367,7 +389,7 @@ func (h *historyHub) add(ch string, pub *Publication, opts PublishOptions) (Stre } pub.Offset = offset - return StreamPosition{Offset: offset, Epoch: epoch}, nil + return StreamPosition{Offset: offset, Epoch: epoch}, prevPub, nil } // Lock must be held outside. @@ -390,7 +412,11 @@ func getPosition(stream *memstream.Stream) StreamPosition { func (h *historyHub) get(ch string, opts HistoryOptions) ([]*Publication, StreamPosition, error) { h.Lock() defer h.Unlock() + return h.getLocked(ch, opts) +} +// Lock must be held outside. +func (h *historyHub) getLocked(ch string, opts HistoryOptions) ([]*Publication, StreamPosition, error) { filter := opts.Filter historyMetaTTL := opts.MetaTTL diff --git a/broker_memory_test.go b/broker_memory_test.go index aff3a46f..5f4725f7 100644 --- a/broker_memory_test.go +++ b/broker_memory_test.go @@ -141,7 +141,7 @@ func TestMemoryBrokerPublishIdempotent(t *testing.T) { numPubs := 0 e.eventHandler = &testBrokerEventHandler{ - HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition) error { + HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { numPubs++ return nil }, @@ -169,7 +169,7 @@ func TestMemoryBrokerPublishIdempotentWithHistory(t *testing.T) { numPubs := 0 e.eventHandler = &testBrokerEventHandler{ - HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition) error { + HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { numPubs++ return nil }, @@ -229,10 +229,10 @@ func TestMemoryHistoryHub(t *testing.T) { ch1 := "channel1" ch2 := "channel2" pub := newTestPublication() - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) - _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second}) - _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) + _, _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second}) + _, _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second}) hist, _, err := h.get(ch1, HistoryOptions{ Filter: HistoryFilter{ @@ -271,10 +271,10 @@ func TestMemoryHistoryHub(t *testing.T) { require.Equal(t, 0, len(hist)) // test history messages limit - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 10, HistoryTTL: time.Second}) - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 10, HistoryTTL: time.Second}) - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 10, HistoryTTL: time.Second}) - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 10, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 10, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 10, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 10, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 10, HistoryTTL: time.Second}) hist, _, err = h.get(ch1, HistoryOptions{ Filter: HistoryFilter{ Limit: -1, @@ -291,8 +291,8 @@ func TestMemoryHistoryHub(t *testing.T) { require.Equal(t, 1, len(hist)) // test history limit greater than history size - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) hist, _, err = h.get(ch1, HistoryOptions{ Filter: HistoryFilter{ Limit: 2, @@ -312,10 +312,10 @@ func TestMemoryHistoryHubMetaTTL(t *testing.T) { h.RLock() require.Equal(t, int64(0), h.nextRemoveCheck) h.RUnlock() - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) - _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second}) - _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second}) + _, _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second}) + _, _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second}) h.RLock() require.True(t, h.nextRemoveCheck > 0) require.Equal(t, 2, len(h.streams)) @@ -350,10 +350,10 @@ func TestMemoryHistoryHubMetaTTLPerChannel(t *testing.T) { h.RLock() require.Equal(t, int64(0), h.nextRemoveCheck) h.RUnlock() - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second, HistoryMetaTTL: time.Second}) - _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second, HistoryMetaTTL: time.Second}) - _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second, HistoryMetaTTL: time.Second}) - _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second, HistoryMetaTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second, HistoryMetaTTL: time.Second}) + _, _, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second, HistoryMetaTTL: time.Second}) + _, _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second, HistoryMetaTTL: time.Second}) + _, _, _ = h.add(ch2, pub, PublishOptions{HistorySize: 2, HistoryTTL: time.Second, HistoryMetaTTL: time.Second}) h.RLock() require.True(t, h.nextRemoveCheck > 0) require.Equal(t, 2, len(h.streams)) @@ -545,19 +545,33 @@ type recoverTest struct { Sleep int Limit int Recovered bool + RecoveryMode RecoveryMode } var clientRecoverTests = []recoverTest{ - {"empty_stream", 10, 60, 0, 0, 0, 0, 0, true}, - {"from_position", 10, 60, 10, 8, 2, 0, 0, true}, - {"from_position_limited", 10, 60, 10, 5, 0, 0, 2, false}, - {"from_position_with_server_limit", 10, 60, 10, 5, 0, 0, 1, false}, - {"from_position_that_already_gone", 10, 60, 20, 8, 0, 0, 0, false}, - {"from_position_that_not_exist_yet", 10, 60, 20, 108, 0, 0, 0, false}, - {"same_position_no_pubs_expected", 10, 60, 7, 7, 0, 0, 0, true}, - {"empty_position_recover_expected", 10, 60, 4, 0, 4, 0, 0, true}, - {"from_position_in_expired_stream", 10, 1, 10, 8, 0, 3, 0, false}, - {"from_same_position_in_expired_stream", 10, 1, 1, 1, 0, 3, 0, true}, + {"empty_stream", 10, 60, 0, 0, 0, 0, 0, true, RecoveryModeStream}, + {"from_position", 10, 60, 10, 8, 2, 0, 0, true, RecoveryModeStream}, + {"from_position_limited", 10, 60, 10, 5, 0, 0, 2, false, RecoveryModeStream}, + {"from_position_with_server_limit", 10, 60, 10, 5, 0, 0, 1, false, RecoveryModeStream}, + {"from_position_that_already_gone", 10, 60, 20, 8, 0, 0, 0, false, RecoveryModeStream}, + {"from_position_that_not_exist_yet", 10, 60, 20, 108, 0, 0, 0, false, RecoveryModeStream}, + {"same_position_no_pubs_expected", 10, 60, 7, 7, 0, 0, 0, true, RecoveryModeStream}, + {"empty_position_recover_expected", 10, 60, 4, 0, 4, 0, 0, true, RecoveryModeStream}, + {"from_position_in_expired_stream", 10, 1, 10, 8, 0, 3, 0, false, RecoveryModeStream}, + {"from_same_position_in_expired_stream", 10, 1, 1, 1, 0, 3, 0, true, RecoveryModeStream}, + {"from_same_position_in_expired_stream", 10, 1, 1, 1, 0, 3, 0, true, RecoveryModeStream}, + + {"cache_empty_stream", 10, 60, 0, 0, 0, 0, 0, false, RecoveryModeCache}, + {"cache_from_position", 10, 60, 10, 8, 1, 0, 0, true, RecoveryModeCache}, + {"cache_from_position_limited", 10, 60, 10, 5, 1, 0, 2, true, RecoveryModeCache}, + {"cache_from_position_with_server_limit", 10, 60, 10, 5, 1, 0, 1, true, RecoveryModeCache}, + {"cache_from_position_that_already_gone", 10, 60, 20, 8, 1, 0, 0, true, RecoveryModeCache}, + {"cache_from_position_that_not_exist_yet", 10, 60, 20, 108, 1, 0, 0, true, RecoveryModeCache}, + {"cache_same_position_no_pubs_expected", 10, 60, 7, 7, 0, 0, 0, true, RecoveryModeCache}, + {"cache_empty_position_recover_expected", 10, 60, 4, 0, 1, 0, 0, true, RecoveryModeCache}, + {"cache_from_position_in_expired_stream", 10, 1, 10, 8, 0, 3, 0, false, RecoveryModeCache}, + {"cache_from_same_position_in_expired_stream", 10, 1, 1, 1, 0, 3, 0, true, RecoveryModeCache}, + {"cache_from_same_position_in_expired_stream", 10, 1, 1, 1, 0, 3, 0, true, RecoveryModeCache}, } type recoverTestChannel struct { @@ -576,7 +590,7 @@ func TestClientSubscribeRecover(t *testing.T) { node.config.RecoveryMaxPublicationLimit = tt.Limit node.OnConnect(func(client *Client) { client.OnSubscribe(func(event SubscribeEvent, cb SubscribeCallback) { - opts := SubscribeOptions{EnableRecovery: true} + opts := SubscribeOptions{EnableRecovery: true, RecoveryMode: tt.RecoveryMode} cb(SubscribeReply{Options: opts}, nil) }) }) @@ -615,8 +629,8 @@ func TestClientSubscribeRecover(t *testing.T) { require.Nil(t, disconnect) require.Nil(t, rwWrapper.replies[0].Error) res := extractSubscribeResult(rwWrapper.replies) - require.Equal(t, tt.NumRecovered, len(res.Publications)) require.Equal(t, tt.Recovered, res.Recovered) + require.Equal(t, tt.NumRecovered, len(res.Publications)) if len(res.Publications) > 1 { require.True(t, res.Publications[0].Offset < res.Publications[1].Offset) } @@ -754,3 +768,19 @@ func BenchmarkMemoryBrokerHistoryIteration(b *testing.B) { it.testHistoryIteration(b, e.node, startPosition) } } + +func TestMemoryHistoryHubPrevPub(t *testing.T) { + t.Parallel() + h := newHistoryHub(0, make(chan struct{})) + h.runCleanups() + h.RLock() + require.Equal(t, 0, len(h.streams)) + h.RUnlock() + defer h.close() + ch1 := "channel1" + pub := newTestPublication() + _, prevPub, _ := h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second, UseDelta: true}) + require.Nil(t, prevPub) + _, prevPub, _ = h.add(ch1, pub, PublishOptions{HistorySize: 1, HistoryTTL: time.Second, UseDelta: true}) + require.NotNil(t, prevPub) +} diff --git a/broker_redis.go b/broker_redis.go index 1457b816..6e3adb5b 100644 --- a/broker_redis.go +++ b/broker_redis.go @@ -710,6 +710,11 @@ func (b *RedisBroker) publish(s *shardWrapper, ch string, data []byte, opts Publ script = b.addHistoryStreamScript } + var useDelta string + if opts.UseDelta { + useDelta = "1" + } + replies, err := script.Exec( context.Background(), s.shard.client, @@ -723,6 +728,7 @@ func (b *RedisBroker) publish(s *shardWrapper, ch string, data []byte, opts Publ strconv.FormatInt(time.Now().Unix(), 10), publishCommand, resultExpire, + useDelta, }, ).ToArray() if err != nil { @@ -996,7 +1002,7 @@ var ( ) func (b *RedisBroker) handleRedisClientMessage(eventHandler BrokerEventHandler, chID channelID, data []byte) error { - pushData, pushType, sp, ok := extractPushData(data) + pushData, pushType, sp, delta, prevPayload, ok := extractPushData(data) if !ok { return fmt.Errorf("malformed PUB/SUB data: %s", data) } @@ -1013,7 +1019,16 @@ func (b *RedisBroker) handleRedisClientMessage(eventHandler BrokerEventHandler, // it to unmarshalled Publication. pub.Offset = sp.Offset } - _ = eventHandler.HandlePublication(channel, pubFromProto(&pub), sp) + if delta && len(prevPayload) > 0 { + var prevPub protocol.Publication + err = prevPub.UnmarshalVT(prevPayload) + if err != nil { + return err + } + _ = eventHandler.HandlePublication(channel, pubFromProto(&pub), sp, pubFromProto(&prevPub)) + } else { + _ = eventHandler.HandlePublication(channel, pubFromProto(&pub), sp, nil) + } } else if pushType == joinPushType { var info protocol.ClientInfo err := info.UnmarshalVT(pushData) @@ -1199,7 +1214,7 @@ func (b *RedisBroker) historyList(s *RedisShard, ch string, filter HistoryFilter return nil, StreamPosition{}, errors.New("error getting value") } - pushData, _, sp, ok := extractPushData(convert.StringToBytes(value)) + pushData, _, sp, _, _, ok := extractPushData(convert.StringToBytes(value)) if !ok { return nil, StreamPosition{}, fmt.Errorf("malformed publication value: %s", value) } @@ -1281,45 +1296,149 @@ var ( ) // See tests for supported format examples. -func extractPushData(data []byte) ([]byte, pushType, StreamPosition, bool) { +func extractPushData(data []byte) ([]byte, pushType, StreamPosition, bool, []byte, bool) { var offset uint64 var epoch string if !bytes.HasPrefix(data, metaSep) { - return data, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, true + return data, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false, nil, true } - nextMetaSepPos := bytes.Index(data[len(metaSep):], metaSep) - if nextMetaSepPos <= 0 { - return data, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false + + content := data[len(metaSep):] + if len(content) == 0 { + return data, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false, nil, false } - content := data[len(metaSep) : len(metaSep)+nextMetaSepPos] - contentType := content[0] - rest := data[len(metaSep)+nextMetaSepPos+len(metaSep):] + contentType := content[0] switch contentType { case 'j': - return rest, joinPushType, StreamPosition{}, true + // __j__payload. + nextMetaSepPos := bytes.Index(data[len(metaSep):], metaSep) + if nextMetaSepPos <= 0 { + return data, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false, nil, false + } + rest := data[len(metaSep)+nextMetaSepPos+len(metaSep):] + return rest, joinPushType, StreamPosition{}, false, nil, true case 'l': - return rest, leavePushType, StreamPosition{}, true - } + // __l__payload. + nextMetaSepPos := bytes.Index(data[len(metaSep):], metaSep) + if nextMetaSepPos <= 0 { + return data, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false, nil, false + } + rest := data[len(metaSep)+nextMetaSepPos+len(metaSep):] + return rest, leavePushType, StreamPosition{}, false, nil, true + case 'p': + // p1:offset:epoch__payload + nextMetaSepPos := bytes.Index(data[len(metaSep):], metaSep) + if nextMetaSepPos <= 0 { + return data, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false, nil, false + } + header := data[len(metaSep) : len(metaSep)+nextMetaSepPos] + stringHeader := convert.BytesToString(header) - stringContent := convert.BytesToString(content) + rest := data[len(metaSep)+nextMetaSepPos+len(metaSep):] - if contentType == 'p' { - // new format p1:offset:epoch - stringContent = stringContent[3:] // offset:epoch - epochDelimiterPos := strings.Index(stringContent, contentSep) + stringHeader = stringHeader[3:] // offset:epoch + epochDelimiterPos := strings.Index(stringHeader, contentSep) if epochDelimiterPos <= 0 { - return rest, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false + return rest, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false, nil, false } var err error - offset, err = strconv.ParseUint(stringContent[:epochDelimiterPos], 10, 64) - epoch = stringContent[epochDelimiterPos+1:] - return rest, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, err == nil + offset, err = strconv.ParseUint(stringHeader[:epochDelimiterPos], 10, 64) + epoch = stringHeader[epochDelimiterPos+1:] + return rest, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false, nil, err == nil + case 'd': + // d1:offset:epoch:prev_payload_length:prev_payload:payload_length:payload + stringContent := convert.BytesToString(content) + parsedDelta, err := parseDeltaPush(stringContent) + return convert.StringToBytes(parsedDelta.Payload), pubPushType, StreamPosition{Epoch: parsedDelta.Epoch, Offset: parsedDelta.Offset}, true, convert.StringToBytes(parsedDelta.PrevPayload), err == nil + default: + // Unknown content type. + return nil, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, false, nil, false } +} - // old format with offset only: __offset__ - var err error - offset, err = strconv.ParseUint(stringContent, 10, 64) - return rest, pubPushType, StreamPosition{Epoch: epoch, Offset: offset}, err == nil +type deltaPublicationPush struct { + Offset uint64 + Epoch string + PrevPayloadLength int + PrevPayload string + PayloadLength int + Payload string +} + +func parseDeltaPush(input string) (*deltaPublicationPush, error) { + // d1:offset:epoch:prev_payload_length:prev_payload:payload_length:payload + const prefix = "d1:" + if !strings.HasPrefix(input, prefix) { + return nil, fmt.Errorf("input does not start with the expected prefix") + } + input = input[len(prefix):] // Remove prefix + + // offset:epoch:prev_payload_length:prev_payload:payload_length:payload + + idx := strings.IndexByte(input, ':') + if idx == -1 { + return nil, fmt.Errorf("invalid format, missing offset") + } + offset, err := strconv.ParseUint(input[:idx], 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing offset: %v", err) + } + input = input[idx+1:] + + // epoch:prev_payload_length:prev_payload:payload_length:payload + + idx = strings.IndexByte(input, ':') + if idx == -1 { + return nil, fmt.Errorf("invalid format, missing epoch") + } + epoch := input[:idx] + input = input[idx+1:] + + // prev_payload_length:prev_payload:payload_length:payload + + idx = strings.IndexByte(input, ':') + if idx == -1 { + return nil, fmt.Errorf("invalid format, missing prev payload length") + } + prevPayloadLength, err := strconv.Atoi(input[:idx]) + if err != nil { + return nil, fmt.Errorf("error parsing prev payload length: %v", err) + } + + input = input[idx+1:] + + // Extract prev_payload based on prev_payload_length + if len(input) < prevPayloadLength { + return nil, fmt.Errorf("input is shorter than expected prev payload length") + } + prevPayload := input[:prevPayloadLength] + input = input[prevPayloadLength+1:] + + // payload_length:payload + idx = strings.IndexByte(input, ':') + if idx == -1 { + return nil, fmt.Errorf("invalid format, missing payload") + } + payloadLength, err := strconv.Atoi(input[:idx]) + if err != nil { + return nil, fmt.Errorf("error parsing payload_length: %v", err) + } + input = input[idx+1:] + + // Extract payload based on payload_length + if len(input) < payloadLength { + return nil, fmt.Errorf("input is shorter than expected payload length") + } + payload := input[:payloadLength] + + return &deltaPublicationPush{ + Offset: offset, + Epoch: epoch, + PrevPayloadLength: prevPayloadLength, + PrevPayload: prevPayload, + PayloadLength: payloadLength, + Payload: payload, + }, nil } diff --git a/broker_redis_test.go b/broker_redis_test.go index f805c6e7..5e82d3bf 100644 --- a/broker_redis_test.go +++ b/broker_redis_test.go @@ -16,6 +16,7 @@ import ( "time" "github.com/centrifugal/protocol" + "github.com/google/uuid" "github.com/stretchr/testify/require" ) @@ -684,7 +685,7 @@ func TestRedisBrokerHandlePubSubMessage(t *testing.T) { b := NewTestRedisBroker(t, node, getUniquePrefix(), false) defer func() { _ = node.Shutdown(context.Background()) }() defer stopRedisBroker(b) - err := b.handleRedisClientMessage(&testBrokerEventHandler{HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition) error { + err := b.handleRedisClientMessage(&testBrokerEventHandler{HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { require.Equal(t, "test", ch) require.Equal(t, uint64(16901), sp.Offset) require.Equal(t, "xyz", sp.Epoch) @@ -692,7 +693,7 @@ func TestRedisBrokerHandlePubSubMessage(t *testing.T) { }}, b.messageChannelID(b.shards[0].shard, "test"), []byte("__p1:16901:xyz__dsdsd")) require.Error(t, err) - err = b.handleRedisClientMessage(&testBrokerEventHandler{HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition) error { + err = b.handleRedisClientMessage(&testBrokerEventHandler{HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { return nil }}, b.messageChannelID(b.shards[0].shard, "test"), []byte("__p1:16901")) require.Error(t, err) @@ -703,7 +704,7 @@ func TestRedisBrokerHandlePubSubMessage(t *testing.T) { data, err := pub.MarshalVT() require.NoError(t, err) var publicationHandlerCalled bool - err = b.handleRedisClientMessage(&testBrokerEventHandler{HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition) error { + err = b.handleRedisClientMessage(&testBrokerEventHandler{HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { publicationHandlerCalled = true require.Equal(t, "test", ch) require.Equal(t, uint64(16901), sp.Offset) @@ -744,7 +745,7 @@ func BenchmarkRedisExtractPushData(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - _, _, sp, ok := extractPushData(data) + _, _, sp, _, _, ok := extractPushData(data) if !ok { b.Fatal("wrong data") } @@ -759,55 +760,40 @@ func BenchmarkRedisExtractPushData(b *testing.B) { func TestRedisExtractPushData(t *testing.T) { data := []byte(`__p1:16901:xyz.123__\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`) - pushData, pushType, sp, ok := extractPushData(data) + pushData, pushType, sp, _, _, ok := extractPushData(data) require.True(t, ok) require.Equal(t, pubPushType, pushType) require.Equal(t, uint64(16901), sp.Offset) require.Equal(t, "xyz.123", sp.Epoch) require.Equal(t, []byte(`\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`), pushData) - data = []byte(`__16901__\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`) - pushData, pushType, sp, ok = extractPushData(data) - require.True(t, ok) - require.Equal(t, pubPushType, pushType) - require.Equal(t, uint64(16901), sp.Offset) - require.Equal(t, "", sp.Epoch) - require.Equal(t, []byte(`\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`), pushData) - data = []byte(`\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`) - pushData, pushType, sp, ok = extractPushData(data) + pushData, pushType, sp, _, _, ok = extractPushData(data) require.True(t, ok) require.Equal(t, pubPushType, pushType) require.Equal(t, uint64(0), sp.Offset) require.Equal(t, []byte(`\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`), pushData) - data = []byte(`__4294967337__\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`) - pushData, pushType, sp, ok = extractPushData(data) - require.True(t, ok) - require.Equal(t, pubPushType, pushType) - require.Equal(t, uint64(4294967337), sp.Offset) - require.Equal(t, []byte(`\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`), pushData) - data = []byte(`__j__\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`) - pushData, pushType, sp, ok = extractPushData(data) + pushData, pushType, sp, _, _, ok = extractPushData(data) require.True(t, ok) require.Equal(t, joinPushType, pushType) require.Equal(t, uint64(0), sp.Offset) require.Equal(t, []byte(`\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`), pushData) data = []byte(`__l__\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`) - pushData, pushType, sp, ok = extractPushData(data) + pushData, pushType, sp, _, _, ok = extractPushData(data) require.True(t, ok) require.Equal(t, leavePushType, pushType) require.Equal(t, uint64(0), sp.Offset) require.Equal(t, []byte(`\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`), pushData) data = []byte(`____\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`) - _, _, _, ok = extractPushData(data) + _, _, _, _, _, ok = extractPushData(data) require.False(t, ok) data = []byte(`__a__\x12\nchat:index\x1aU\"\x0e{\"input\":\"__\"}*C\n\x0242\x12$37cb00a9-bcfa-4284-a1ae-607c7da3a8f4\x1a\x15{\"name\": \"Alexander\"}\"\x00`) - _, _, _, ok = extractPushData(data) + _, _, _, _, _, ok = extractPushData(data) require.False(t, ok) } @@ -973,7 +959,7 @@ func TestRedisPubSubTwoNodes(t *testing.T) { HandleControlFunc: func(bytes []byte) error { return nil }, - HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition) error { + HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { c := atomic.AddInt64(&numPublications, 1) if c == int64(msgNum) { close(pubCh) @@ -1040,6 +1026,96 @@ func TestRedisPubSubTwoNodes(t *testing.T) { } } +type testDeltaPublishHandle struct { + ch string + pub *Publication + sp StreamPosition + prevPub *Publication +} + +func TestRedisPubSubTwoNodesWithDelta(t *testing.T) { + redisConf := testRedisConf() + + prefix := getUniquePrefix() + + ch := "test" + uuid.NewString() + + node1, _ := New(Config{}) + s, err := NewRedisShard(node1, redisConf) + require.NoError(t, err) + b1, _ := NewRedisBroker(node1, RedisBrokerConfig{ + Prefix: prefix, + Shards: []*RedisShard{s}, + numPubSubSubscribers: 4, + numPubSubProcessors: 2, + }) + node1.SetBroker(b1) + defer func() { _ = node1.Shutdown(context.Background()) }() + defer stopRedisBroker(b1) + + msgNum := 2 + var numPublications int64 + pubCh := make(chan struct{}) + var resultsMu sync.Mutex + results := make([]testDeltaPublishHandle, 0, msgNum) + + brokerEventHandler := &testBrokerEventHandler{ + HandleControlFunc: func(bytes []byte) error { + return nil + }, + HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { + resultsMu.Lock() + defer resultsMu.Unlock() + results = append(results, testDeltaPublishHandle{ + ch: ch, + pub: pub, + sp: sp, + prevPub: prevPub, + }) + c := atomic.AddInt64(&numPublications, 1) + if c == int64(msgNum) { + close(pubCh) + } + return nil + }, + } + _ = b1.Run(brokerEventHandler) + + require.NoError(t, b1.Subscribe(ch)) + + node2, _ := New(Config{}) + s2, err := NewRedisShard(node2, redisConf) + require.NoError(t, err) + + b2, _ := NewRedisBroker(node2, RedisBrokerConfig{ + Prefix: prefix, + Shards: []*RedisShard{s2}, + }) + node2.SetBroker(b2) + _ = node2.Run() + defer func() { _ = node2.Shutdown(context.Background()) }() + defer stopRedisBroker(b2) + + for i := 0; i < msgNum; i++ { + sp, err := node2.Publish(ch, []byte("123"), + WithHistory(1, time.Minute), WithDelta(true)) + require.NoError(t, err) + require.Equal(t, sp.Offset, uint64(i+1)) + } + + select { + case <-pubCh: + case <-time.After(time.Second): + require.Fail(t, "timeout waiting for PUB/SUB message") + } + + resultsMu.Lock() + defer resultsMu.Unlock() + require.Len(t, results, msgNum) + require.Nil(t, results[0].prevPub) + require.NotNil(t, results[1].prevPub) +} + func TestRedisClusterShardedPubSub(t *testing.T) { redisConf := RedisShardConfig{ ClusterAddresses: []string{"localhost:7000", "localhost:7001", "localhost:7002"}, @@ -1080,7 +1156,7 @@ func TestRedisClusterShardedPubSub(t *testing.T) { HandleControlFunc: func(bytes []byte) error { return nil }, - HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition) error { + HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { c := atomic.AddInt64(&numPublications, 1) if c == int64(msgNum) { close(pubCh) @@ -1536,22 +1612,22 @@ func testRedisClientSubscribeRecover(t *testing.T, tt recoverTest, useStreams bo historyResult, err := node.recoverHistory(channel, StreamPosition{tt.SinceOffset, streamTop.Epoch}, 0) require.NoError(t, err) - recoveredPubs, recovered := isRecovered(historyResult, tt.SinceOffset, streamTop.Epoch) + recoveredPubs, recovered := isStreamRecovered(historyResult, tt.SinceOffset, streamTop.Epoch) require.Equal(t, tt.NumRecovered, len(recoveredPubs)) require.Equal(t, tt.Recovered, recovered) } var brokerRecoverTests = []recoverTest{ - {"empty_stream", 10, 60, 0, 0, 0, 0, 0, true}, - {"from_position", 10, 60, 10, 8, 2, 0, 0, true}, - {"from_position_limited", 10, 60, 10, 5, 2, 0, 2, false}, - {"from_position_with_server_limit", 10, 60, 10, 5, 1, 0, 1, false}, - {"from_position_that_already_gone", 10, 60, 20, 8, 10, 0, 0, false}, - {"from_position_that_not_exist_yet", 10, 60, 20, 108, 0, 0, 0, false}, - {"same_position_no_pubs_expected", 10, 60, 7, 7, 0, 0, 0, true}, - {"empty_position_recover_expected", 10, 60, 4, 0, 4, 0, 0, true}, - {"from_position_in_expired_stream", 10, 1, 10, 8, 0, 3, 0, false}, - {"from_same_position_in_expired_stream", 10, 1, 1, 1, 0, 3, 0, true}, + {"empty_stream", 10, 60, 0, 0, 0, 0, 0, true, RecoveryModeStream}, + {"from_position", 10, 60, 10, 8, 2, 0, 0, true, RecoveryModeStream}, + {"from_position_limited", 10, 60, 10, 5, 2, 0, 2, false, RecoveryModeStream}, + {"from_position_with_server_limit", 10, 60, 10, 5, 1, 0, 1, false, RecoveryModeStream}, + {"from_position_that_already_gone", 10, 60, 20, 8, 10, 0, 0, false, RecoveryModeStream}, + {"from_position_that_not_exist_yet", 10, 60, 20, 108, 0, 0, 0, false, RecoveryModeStream}, + {"same_position_no_pubs_expected", 10, 60, 7, 7, 0, 0, 0, true, RecoveryModeStream}, + {"empty_position_recover_expected", 10, 60, 4, 0, 4, 0, 0, true, RecoveryModeStream}, + {"from_position_in_expired_stream", 10, 1, 10, 8, 0, 3, 0, false, RecoveryModeStream}, + {"from_same_position_in_expired_stream", 10, 1, 1, 1, 0, 3, 0, true, RecoveryModeStream}, } func TestRedisClientSubscribeRecoverStreams(t *testing.T) { @@ -1708,7 +1784,7 @@ func BenchmarkPubSubThroughput(b *testing.B) { HandleControlFunc: func(bytes []byte) error { return nil }, - HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition) error { + HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { pubCh <- struct{}{} return nil }, @@ -1883,3 +1959,76 @@ func TestPreShardedSlots(t *testing.T) { }) } } + +func TestParseDeltaPush(t *testing.T) { + tests := []struct { + name string + input string + expectError bool + expectedResult *deltaPublicationPush + }{ + { + name: "valid data with colon in payload", + input: "d1:1234567890:epoch1:4:test:18:payload:with:colon", + expectError: false, + expectedResult: &deltaPublicationPush{ + Offset: 1234567890, + Epoch: "epoch1", + PrevPayloadLength: 4, + PrevPayload: "test", + PayloadLength: 18, + Payload: "payload:with:colon", + }, + }, + { + name: "valid data with empty payload", + input: "d1:1234567890:epoch2:0::0:", + expectError: false, + expectedResult: &deltaPublicationPush{ + Offset: 1234567890, + Epoch: "epoch2", + PrevPayloadLength: 0, + PrevPayload: "", + PayloadLength: 0, + Payload: "", + }, + }, + { + name: "invalid format - missing parts", + input: "d1:123456:epoch3", + expectError: true, + }, + { + name: "invalid offset", + input: "d1:notanumber:epoch4:4:test:5:hello", + expectError: true, + }, + { + name: "invalid prev payload length", + input: "d1:12:epoch4:invalid:test:5:hello", + expectError: true, + }, + { + name: "invalid prev payload length", + input: "d1:12:epoch4:4:test:invalid:hello", + expectError: true, + }, + { + name: "invalid format no payload", + input: "d1:12:epoch4:4:test:5:", + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := parseDeltaPush(tc.input) + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedResult, result) + } + }) + } +} diff --git a/channel_medium.go b/channel_medium.go new file mode 100644 index 00000000..5ceeee9a --- /dev/null +++ b/channel_medium.go @@ -0,0 +1,446 @@ +package centrifuge + +import ( + "errors" + "math" + "sync" + "time" + + "github.com/centrifugal/centrifuge/internal/timers" +) + +// ChannelMediumOptions is an EXPERIMENTAL way to enable using a channel medium layer in Centrifuge. +// Note, channel medium layer is very unstable at the moment – do not use it in production! +// Channel medium layer is an optional per-channel intermediary between Broker PUB/SUB and Client +// connections. This intermediary layer may be used for various per-channel tweaks and optimizations. +// Channel medium comes with memory overhead depending on ChannelMediumOptions. At the same time, it +// can provide significant benefits in terms of overall system efficiency and flexibility. +type ChannelMediumOptions struct { + // KeepLatestPublication enables keeping latest publication which was broadcasted to channel subscribers on + // this Node in the channel medium layer. This is helpful for supporting deltas in at most once scenario. + KeepLatestPublication bool + + // EnablePositionSync when true delegates connection position checks to the channel medium. In that case + // check is only performed no more often than once in Config.ClientChannelPositionCheckDelay thus reducing + // the load on broker in cases when channel has many subscribers. When message loss is detected medium layer + // tells caller about this and also marks all channel subscribers with insufficient state flag. By default, + // medium is not used for sync – in that case each individual connection syncs position independently. + EnablePositionSync bool + + // EnableQueue for incoming publications. This can be useful to reduce PUB/SUB message processing time + // (as we put it into a single medium layer queue instead of each individual connection queue), reduce + // channel broadcast contention (when one channel waits for broadcast of another channel to finish), + // and also opens a road for broadcast tweaks – such as BroadcastDelay and delta between several + // publications (deltas require both BroadcastDelay and KeepLatestPublication to be enabled). This costs + // additional goroutine. + enableQueue bool + // QueueMaxSize is a maximum size of the queue used in channel medium (in bytes). If zero, 16MB default + // is used. If max size reached, new publications will be dropped. + queueMaxSize int + + // BroadcastDelay controls the delay before Publication broadcast. On time tick Centrifugo broadcasts + // only the latest publication in the channel if any. Useful to reduce/smooth the number of messages sent + // to clients when publication contains the entire state. If zero, all publications will be sent to clients + // without delay logic involved on channel medium level. BroadcastDelay option requires (!) EnableQueue to be + // enabled, as we can not afford delays during broadcast from the PUB/SUB layer. BroadcastDelay must not be + // used in channels with positioning/recovery on since it skips publications. + broadcastDelay time.Duration +} + +// Keep global to save 8 byte per-channel. Must be only changed by tests. +var channelMediumTimeNow = time.Now + +// channelMedium is initialized when first subscriber comes into channel, and dropped as soon as last +// subscriber leaves the channel on the Node. +type channelMedium struct { + channel string + node node + options ChannelMediumOptions + + mu sync.RWMutex + closeCh chan struct{} + // optional queue for publications. + messages *publicationQueue + // We must synchronize broadcast method between general publications and insufficient state notifications. + // Only used when queue is disabled. + broadcastMu sync.Mutex + // latestPublication is a publication last sent to connections on this Node. + latestPublication *Publication + // positionCheckTime is a time (Unix Nanoseconds) when last position check was performed. + positionCheckTime int64 +} + +type node interface { + handlePublication(ch string, sp StreamPosition, pub, prevPub *Publication, memPrevPub *Publication) error + streamTop(ch string, historyMetaTTL time.Duration) (StreamPosition, error) +} + +func newChannelMedium(channel string, node node, options ChannelMediumOptions) (*channelMedium, error) { + if options.broadcastDelay > 0 && !options.enableQueue { + return nil, errors.New("broadcast delay can only be used with queue enabled") + } + c := &channelMedium{ + channel: channel, + node: node, + options: options, + closeCh: make(chan struct{}), + positionCheckTime: channelMediumTimeNow().UnixNano(), + } + if options.enableQueue { + c.messages = newPublicationQueue(2) + go c.writer() + } + return c, nil +} + +type queuedPub struct { + pub *Publication + sp StreamPosition + prevPub *Publication + isInsufficientState bool +} + +const defaultChannelLayerQueueMaxSize = 16 * 1024 * 1024 + +func (c *channelMedium) broadcastPublication(pub *Publication, sp StreamPosition, prevPub *Publication) { + bp := queuedPub{pub: pub, sp: sp, prevPub: prevPub} + c.mu.Lock() + c.positionCheckTime = channelMediumTimeNow().UnixNano() + c.mu.Unlock() + + if c.options.enableQueue { + queueMaxSize := defaultChannelLayerQueueMaxSize + if c.options.queueMaxSize > 0 { + queueMaxSize = c.options.queueMaxSize + } + if c.messages.Size() > queueMaxSize { + return + } + c.messages.Add(queuedPublication{Publication: bp}) + } else { + c.broadcastMu.Lock() + defer c.broadcastMu.Unlock() + c.broadcast(bp) + } +} + +func (c *channelMedium) broadcastInsufficientState() { + bp := queuedPub{prevPub: nil, isInsufficientState: true} + c.mu.Lock() + c.positionCheckTime = channelMediumTimeNow().UnixNano() + c.mu.Unlock() + if c.options.enableQueue { + // TODO: possibly support c.messages.dropQueued() for this path ? + c.messages.Add(queuedPublication{Publication: bp}) + } else { + c.broadcastMu.Lock() + defer c.broadcastMu.Unlock() + c.broadcast(bp) + } +} + +func (c *channelMedium) broadcast(qp queuedPub) { + pubToBroadcast := qp.pub + spToBroadcast := qp.sp + if qp.isInsufficientState { + // using math.MaxUint64 as a special offset to trigger insufficient state. + pubToBroadcast = &Publication{Offset: math.MaxUint64} + spToBroadcast.Offset = math.MaxUint64 + } + + prevPub := qp.prevPub + var localPrevPub *Publication + useLocalLatestPub := c.options.KeepLatestPublication && !qp.isInsufficientState + if useLocalLatestPub { + localPrevPub = c.latestPublication + } + if c.options.broadcastDelay > 0 && !c.options.KeepLatestPublication { + prevPub = nil + } + if qp.isInsufficientState { + prevPub = nil + } + _ = c.node.handlePublication(c.channel, spToBroadcast, pubToBroadcast, prevPub, localPrevPub) + if useLocalLatestPub { + c.latestPublication = qp.pub + } +} + +func (c *channelMedium) writer() { + for { + if ok := c.waitSendPub(c.options.broadcastDelay); !ok { + return + } + } +} + +func (c *channelMedium) waitSendPub(delay time.Duration) bool { + // Wait for message from the queue. + ok := c.messages.Wait() + if !ok { + return false + } + + if delay > 0 { + tm := timers.AcquireTimer(delay) + select { + case <-tm.C: + case <-c.closeCh: + timers.ReleaseTimer(tm) + return false + } + timers.ReleaseTimer(tm) + } + + msg, ok := c.messages.Remove() + if !ok { + return !c.messages.Closed() + } + if delay == 0 || msg.Publication.isInsufficientState { + c.broadcast(msg.Publication) + return true + } + messageCount := c.messages.Len() + for messageCount > 0 { + messageCount-- + var ok bool + msg, ok = c.messages.Remove() + if !ok { + if c.messages.Closed() { + return false + } + break + } + if msg.Publication.isInsufficientState { + break + } + } + c.broadcast(msg.Publication) + return true +} + +func (c *channelMedium) CheckPosition(historyMetaTTL time.Duration, clientPosition StreamPosition, checkDelay time.Duration) bool { + nowUnixNano := channelMediumTimeNow().UnixNano() + c.mu.Lock() + needCheckPosition := nowUnixNano-c.positionCheckTime >= checkDelay.Nanoseconds() + if needCheckPosition { + c.positionCheckTime = nowUnixNano + } + c.mu.Unlock() + if !needCheckPosition { + return true + } + _, validPosition, err := c.checkPositionWithRetry(historyMetaTTL, clientPosition) + if err != nil { + // Position will be checked again later. + return true + } + if !validPosition { + c.broadcastInsufficientState() + } + return validPosition +} + +func (c *channelMedium) checkPositionWithRetry(historyMetaTTL time.Duration, clientPosition StreamPosition) (StreamPosition, bool, error) { + sp, validPosition, err := c.checkPositionOnce(historyMetaTTL, clientPosition) + if err != nil || !validPosition { + return c.checkPositionOnce(historyMetaTTL, clientPosition) + } + return sp, validPosition, err +} + +func (c *channelMedium) checkPositionOnce(historyMetaTTL time.Duration, clientPosition StreamPosition) (StreamPosition, bool, error) { + streamTop, err := c.node.streamTop(c.channel, historyMetaTTL) + if err != nil { + return StreamPosition{}, false, err + } + c.mu.Lock() + defer c.mu.Unlock() + isValidPosition := streamTop.Epoch == clientPosition.Epoch && clientPosition.Offset == streamTop.Offset + return streamTop, isValidPosition, nil +} + +func (c *channelMedium) close() { + close(c.closeCh) +} + +type queuedPublication struct { + Publication queuedPub +} + +// publicationQueue is an unbounded queue of queuedPublication. +// The queue is goroutine safe. +// Inspired by http://blog.dubbelboer.com/2015/04/25/go-faster-queue.html (MIT) +type publicationQueue struct { + mu sync.RWMutex + cond *sync.Cond + nodes []queuedPublication + head int + tail int + cnt int + size int + closed bool + initCap int +} + +// newPublicationQueue returns a new queuedPublication queue with initial capacity. +func newPublicationQueue(initialCapacity int) *publicationQueue { + sq := &publicationQueue{ + initCap: initialCapacity, + nodes: make([]queuedPublication, initialCapacity), + } + sq.cond = sync.NewCond(&sq.mu) + return sq +} + +// Mutex must be held when calling. +func (q *publicationQueue) resize(n int) { + nodes := make([]queuedPublication, n) + if q.head < q.tail { + copy(nodes, q.nodes[q.head:q.tail]) + } else { + copy(nodes, q.nodes[q.head:]) + copy(nodes[len(q.nodes)-q.head:], q.nodes[:q.tail]) + } + + q.tail = q.cnt % n + q.head = 0 + q.nodes = nodes +} + +// Add an queuedPublication to the back of the queue +// will return false if the queue is closed. +// In that case the queuedPublication is dropped. +func (q *publicationQueue) Add(i queuedPublication) bool { + q.mu.Lock() + if q.closed { + q.mu.Unlock() + return false + } + if q.cnt == len(q.nodes) { + // Also tested a growth rate of 1.5, see: http://stackoverflow.com/questions/2269063/buffer-growth-strategy + // In Go this resulted in a higher memory usage. + q.resize(q.cnt * 2) + } + q.nodes[q.tail] = i + q.tail = (q.tail + 1) % len(q.nodes) + if i.Publication.pub != nil { + q.size += len(i.Publication.pub.Data) + } + q.cnt++ + q.cond.Signal() + q.mu.Unlock() + return true +} + +// Close the queue and discard all entries in the queue +// all goroutines in wait() will return +func (q *publicationQueue) Close() { + q.mu.Lock() + defer q.mu.Unlock() + q.closed = true + q.cnt = 0 + q.nodes = nil + q.size = 0 + q.cond.Broadcast() +} + +// CloseRemaining will close the queue and return all entries in the queue. +// All goroutines in wait() will return. +func (q *publicationQueue) CloseRemaining() []queuedPublication { + q.mu.Lock() + defer q.mu.Unlock() + if q.closed { + return []queuedPublication{} + } + rem := make([]queuedPublication, 0, q.cnt) + for q.cnt > 0 { + i := q.nodes[q.head] + q.head = (q.head + 1) % len(q.nodes) + q.cnt-- + rem = append(rem, i) + } + q.closed = true + q.cnt = 0 + q.nodes = nil + q.size = 0 + q.cond.Broadcast() + return rem +} + +// Closed returns true if the queue has been closed +// The call cannot guarantee that the queue hasn't been +// closed while the function returns, so only "true" has a definite meaning. +func (q *publicationQueue) Closed() bool { + q.mu.RLock() + c := q.closed + q.mu.RUnlock() + return c +} + +// Wait for a message to be added. +// If there are items on the queue will return immediately. +// Will return false if the queue is closed. +// Otherwise, returns true. +func (q *publicationQueue) Wait() bool { + q.mu.Lock() + if q.closed { + q.mu.Unlock() + return false + } + if q.cnt != 0 { + q.mu.Unlock() + return true + } + q.cond.Wait() + q.mu.Unlock() + return true +} + +// Remove will remove an queuedPublication from the queue. +// If false is returned, it either means 1) there were no items on the queue +// or 2) the queue is closed. +func (q *publicationQueue) Remove() (queuedPublication, bool) { + q.mu.Lock() + if q.cnt == 0 { + q.mu.Unlock() + return queuedPublication{}, false + } + i := q.nodes[q.head] + q.head = (q.head + 1) % len(q.nodes) + q.cnt-- + if i.Publication.pub != nil { + q.size -= len(i.Publication.pub.Data) + } + + if n := len(q.nodes) / 2; n >= q.initCap && q.cnt <= n { + q.resize(n) + } + + q.mu.Unlock() + return i, true +} + +// Cap returns the capacity (without allocations) +func (q *publicationQueue) Cap() int { + q.mu.RLock() + c := cap(q.nodes) + q.mu.RUnlock() + return c +} + +// Len returns the current length of the queue. +func (q *publicationQueue) Len() int { + q.mu.RLock() + l := q.cnt + q.mu.RUnlock() + return l +} + +// Size returns the current size of the queue. +func (q *publicationQueue) Size() int { + q.mu.RLock() + s := q.size + q.mu.RUnlock() + return s +} diff --git a/channel_medium_test.go b/channel_medium_test.go new file mode 100644 index 00000000..b1e9be86 --- /dev/null +++ b/channel_medium_test.go @@ -0,0 +1,173 @@ +package centrifuge + +import ( + "errors" + "math" + "strconv" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// Helper function to create a channelMedium with options. +func setupChannelMedium(t testing.TB, options ChannelMediumOptions, node node) *channelMedium { + t.Helper() + channel := "testChannel" + cache, err := newChannelMedium(channel, node, options) + if err != nil { + require.NoError(t, err) + } + return cache +} + +type mockNode struct { + handlePublicationFunc func(channel string, sp StreamPosition, pub, prevPub, localPrevPub *Publication) error + streamTopFunc func(ch string, historyMetaTTL time.Duration) (StreamPosition, error) +} + +func (m *mockNode) handlePublication(channel string, sp StreamPosition, pub, prevPub, localPrevPub *Publication) error { + if m.handlePublicationFunc != nil { + return m.handlePublicationFunc(channel, sp, pub, prevPub, localPrevPub) + } + return nil +} + +func (m *mockNode) streamTop(ch string, historyMetaTTL time.Duration) (StreamPosition, error) { + if m.streamTopFunc != nil { + return m.streamTopFunc(ch, historyMetaTTL) + } + return StreamPosition{}, nil +} + +func TestChannelMediumHandlePublication(t *testing.T) { + optionSet := []ChannelMediumOptions{ + { + enableQueue: false, + KeepLatestPublication: false, + }, + { + enableQueue: true, + KeepLatestPublication: false, + }, + { + enableQueue: true, + KeepLatestPublication: false, + broadcastDelay: 10 * time.Millisecond, + }, + { + enableQueue: true, + KeepLatestPublication: true, + broadcastDelay: 10 * time.Millisecond, + }, + } + + for i, options := range optionSet { + t.Run(strconv.Itoa(i), func(t *testing.T) { + doneCh := make(chan struct{}) + + cache := setupChannelMedium(t, options, &mockNode{ + handlePublicationFunc: func(channel string, sp StreamPosition, pub, prevPub, localPrevPub *Publication) error { + close(doneCh) + return nil + }, + }) + + pub := &Publication{Data: []byte("test data")} + sp := StreamPosition{Offset: 1} + + cache.broadcastPublication(pub, sp, nil) + + select { + case <-doneCh: + case <-time.After(5 * time.Second): + require.Fail(t, "handlePublicationFunc was not called") + } + }) + } +} + +func TestChannelMediumInsufficientState(t *testing.T) { + options := ChannelMediumOptions{ + enableQueue: true, + KeepLatestPublication: true, + } + doneCh := make(chan struct{}) + medium := setupChannelMedium(t, options, &mockNode{ + handlePublicationFunc: func(channel string, sp StreamPosition, pub, prevPub, localPrevPub *Publication) error { + require.Equal(t, uint64(math.MaxUint64), pub.Offset) + require.Equal(t, uint64(math.MaxUint64), sp.Offset) + close(doneCh) + return nil + }, + }) + + // Simulate the behavior when the state is marked as insufficient + medium.broadcastInsufficientState() + + select { + case <-doneCh: + case <-time.After(5 * time.Second): + require.Fail(t, "handlePublicationFunc was not called") + } +} + +func TestChannelMediumPositionSync(t *testing.T) { + options := ChannelMediumOptions{ + EnablePositionSync: true, + } + doneCh := make(chan struct{}) + var closeOnce sync.Once + medium := setupChannelMedium(t, options, &mockNode{ + streamTopFunc: func(ch string, historyMetaTTL time.Duration) (StreamPosition, error) { + closeOnce.Do(func() { + close(doneCh) + }) + return StreamPosition{}, nil + }, + }) + originalGetter := channelMediumTimeNow + channelMediumTimeNow = func() time.Time { + return time.Now().Add(time.Hour) + } + medium.CheckPosition(time.Second, StreamPosition{Offset: 1, Epoch: "test"}, time.Second) + channelMediumTimeNow = originalGetter + select { + case <-doneCh: + case <-time.After(5 * time.Second): + require.Fail(t, "historyFunc was not called") + } +} + +func TestChannelMediumPositionSyncRetry(t *testing.T) { + options := ChannelMediumOptions{ + EnablePositionSync: true, + } + doneCh := make(chan struct{}) + var closeOnce sync.Once + numCalls := 0 + medium := setupChannelMedium(t, options, &mockNode{ + streamTopFunc: func(ch string, historyMetaTTL time.Duration) (StreamPosition, error) { + if numCalls == 0 { + numCalls++ + return StreamPosition{}, errors.New("boom") + } + closeOnce.Do(func() { + close(doneCh) + }) + return StreamPosition{}, nil + }, + }) + originalGetter := channelMediumTimeNow + channelMediumTimeNow = func() time.Time { + return time.Now().Add(time.Hour) + } + medium.CheckPosition(time.Second, StreamPosition{Offset: 1, Epoch: "test"}, time.Second) + channelMediumTimeNow = originalGetter + select { + case <-doneCh: + case <-time.After(5 * time.Second): + require.Fail(t, "streamTopLatestPubFunc was not called") + } +} diff --git a/client.go b/client.go index 954fb277..f1a640f5 100644 --- a/client.go +++ b/client.go @@ -5,9 +5,12 @@ import ( "errors" "fmt" "io" + "math" + "slices" "sync" "time" + "github.com/centrifugal/centrifuge/internal/convert" "github.com/centrifugal/centrifuge/internal/queue" "github.com/centrifugal/centrifuge/internal/recovery" "github.com/centrifugal/centrifuge/internal/saferand" @@ -15,6 +18,7 @@ import ( "github.com/centrifugal/protocol" "github.com/google/uuid" "github.com/segmentio/encoding/json" + fdelta "github.com/shadowspore/fossil-delta" ) // Empty Replies/Pushes for pings. @@ -136,6 +140,7 @@ const ( flagPositioning flagServerSide flagClientSideRefresh + flagDeltaAllowed ) // ChannelContext contains extra context for channel connection subscribed to. @@ -527,11 +532,11 @@ func (c *Client) checkPong() { func (c *Client) addPingUpdate(isFirst bool, scheduleNext bool) { delay := c.pingInterval if isFirst { - // Send first ping in random interval between 0 and PingInterval to + // Send first ping in random interval between PingInterval/2 and PingInterval to // spread ping-pongs in time (useful when many connections reconnect // almost immediately). pingNanoseconds := c.pingInterval.Nanoseconds() - delay = time.Duration(randSource.Int63n(pingNanoseconds)) * time.Nanosecond + delay = time.Duration(pingNanoseconds/2) + time.Duration(randSource.Int63n(pingNanoseconds/2))*time.Nanosecond } c.nextPing = time.Now().Add(delay).UnixNano() if scheduleNext { @@ -737,16 +742,6 @@ func (c *Client) checkPosition(checkDelay time.Duration, ch string, chCtx Channe historyMetaTTL = time.Duration(chCtx.metaTTLSeconds) * time.Second } - streamTop, err := c.node.streamTop(ch, historyMetaTTL) - if err != nil { - // Check later. - return true - } - - return c.isValidPosition(streamTop, nowUnix, ch) -} - -func (c *Client) isValidPosition(streamTop StreamPosition, nowUnix int64, ch string) bool { c.mu.Lock() if c.status == statusClosed { c.mu.Unlock() @@ -760,18 +755,20 @@ func (c *Client) isValidPosition(streamTop StreamPosition, nowUnix int64, ch str position := chCtx.streamPosition c.mu.Unlock() - isValidPosition := streamTop.Epoch == position.Epoch && position.Offset >= streamTop.Offset - if isValidPosition { + validPosition, err := c.node.checkPosition(ch, position, historyMetaTTL) + if err != nil { + // Check later. + return true + } + if validPosition { c.mu.Lock() if chContext, ok := c.channels[ch]; ok { chContext.positionCheckTime = nowUnix c.channels[ch] = chContext } c.mu.Unlock() - return true } - - return false + return validPosition } // ID returns unique client connection id. @@ -1613,6 +1610,17 @@ func (c *Client) handleSubscribe(req *protocol.SubscribeRequest, cmd *protocol.C return ErrorNotAvailable } + if req.Channel == "" { + return c.logDisconnectBadRequest("channel required for subscribe") + } + + if req.Delta != "" { + _, ok := stringToDeltaType[req.Delta] + if !ok { + return c.logDisconnectBadRequest("unknown delta type in subscribe request: " + req.Delta) + } + } + replyError, disconnect := c.validateSubscribeRequest(req) if disconnect != nil || replyError != nil { if disconnect != nil { @@ -2666,7 +2674,7 @@ type subscribeContext struct { channelContext ChannelContext } -func isRecovered(historyResult HistoryResult, cmdOffset uint64, cmdEpoch string) ([]*protocol.Publication, bool) { +func isStreamRecovered(historyResult HistoryResult, cmdOffset uint64, cmdEpoch string) ([]*protocol.Publication, bool) { latestOffset := historyResult.Offset latestEpoch := historyResult.Epoch @@ -2689,6 +2697,26 @@ func isRecovered(historyResult HistoryResult, cmdOffset uint64, cmdEpoch string) return recoveredPubs, recovered } +func isCacheRecovered(latestPub *Publication, currentSP StreamPosition, cmdOffset uint64, cmdEpoch string) ([]*protocol.Publication, bool) { + latestOffset := currentSP.Offset + latestEpoch := currentSP.Epoch + var recovered bool + recoveredPubs := make([]*protocol.Publication, 0, 1) + if latestPub != nil { + publication := latestPub + recovered = publication.Offset == latestOffset + skipPublication := cmdOffset > 0 && cmdOffset == latestOffset && cmdEpoch == latestEpoch + if recovered && !skipPublication { + protoPub := pubToProto(publication) + recoveredPubs = append(recoveredPubs, protoPub) + } + } else if cmdOffset > 0 && latestOffset == cmdOffset && cmdEpoch == latestEpoch { + // Client already had state, which has not been modified since. + recovered = true + } + return recoveredPubs, recovered +} + // subscribeCmd handles subscribe command - clients send this when subscribe // on channel, if channel is private then we must validate provided sign here before // actually subscribe client on channel. Optionally we can send missed messages to @@ -2731,7 +2759,15 @@ func (c *Client) subscribeCmd(req *protocol.SubscribeRequest, reply SubscribeRep c.pubSubSync.StartBuffering(channel) } - err := c.node.addSubscription(channel, c) + sub := subInfo{client: c, deltaType: deltaTypeNone} + if req.Delta != "" { + dt := DeltaType(req.Delta) + if slices.Contains(reply.Options.AllowedDeltaTypes, dt) { + res.Delta = true + sub.deltaType = dt + } + } + err := c.node.addSubscription(channel, sub) if err != nil { c.node.logger.log(newLogEntry(LogLevelError, "error adding subscription", map[string]any{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()})) c.pubSubSync.StopBuffering(channel) @@ -2759,6 +2795,16 @@ func (c *Client) subscribeCmd(req *protocol.SubscribeRequest, reply SubscribeRep ) if reply.Options.EnablePositioning || reply.Options.EnableRecovery { + handleErr := func(err error) subscribeContext { + c.pubSubSync.StopBuffering(channel) + var clientErr *Error + if errors.As(err, &clientErr) && !errors.Is(clientErr, ErrorInternal) { + return errorDisconnectContext(clientErr, nil) + } + ctx.disconnect = &DisconnectServerError + return ctx + } + res.Positioned = true if reply.Options.EnableRecovery { res.Recoverable = true @@ -2767,45 +2813,74 @@ func (c *Client) subscribeCmd(req *protocol.SubscribeRequest, reply SubscribeRep if reply.Options.EnableRecovery && req.Recover { cmdOffset := req.Offset cmdEpoch := req.Epoch + recoveryMode := reply.Options.RecoveryMode // Client provided subscribe request with recover flag on. Try to recover missed - // publications automatically from history (we suppose here that history configured wisely). - historyResult, err := c.node.recoverHistory(channel, StreamPosition{Offset: cmdOffset, Epoch: cmdEpoch}, reply.Options.HistoryMetaTTL) - if err != nil { - if errors.Is(err, ErrorUnrecoverablePosition) { - // Result contains stream position in case of ErrorUnrecoverablePosition - // during recovery. + // publications automatically from history (we assume here that the history configured wisely). + + if recoveryMode == RecoveryModeCache { + latestPub, currentSP, err := c.node.recoverCache(channel, reply.Options.HistoryMetaTTL) + if err != nil { + c.node.logger.log(newLogEntry(LogLevelError, "error on cache recover", map[string]any{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()})) + return handleErr(err) + } + latestOffset = currentSP.Offset + latestEpoch = currentSP.Epoch + var recovered bool + recoveredPubs, recovered = isCacheRecovered(latestPub, currentSP, cmdOffset, cmdEpoch) + res.Recovered = recovered + if latestPub == nil && c.node.clientEvents.cacheEmptyHandler != nil { + cacheReply, err := c.node.clientEvents.cacheEmptyHandler(CacheEmptyEvent{Channel: channel}) + if err != nil { + c.node.logger.log(newLogEntry(LogLevelError, "error on cache empty", map[string]any{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()})) + return handleErr(err) + } + if cacheReply.Populated && !recovered { + // One more chance to recover in case we know cache was populated. + latestPub, currentSP, err = c.node.recoverCache(channel, reply.Options.HistoryMetaTTL) + if err != nil { + c.node.logger.log(newLogEntry(LogLevelError, "error on populated cache recover", map[string]any{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()})) + return handleErr(err) + } + latestOffset = currentSP.Offset + latestEpoch = currentSP.Epoch + recoveredPubs, recovered = isCacheRecovered(latestPub, currentSP, cmdOffset, cmdEpoch) + res.Recovered = recovered + c.node.metrics.incRecover(res.Recovered) + } else { + c.node.metrics.incRecover(res.Recovered) + } + } else { + c.node.metrics.incRecover(res.Recovered) + } + } else { + historyResult, err := c.node.recoverHistory(channel, StreamPosition{Offset: cmdOffset, Epoch: cmdEpoch}, reply.Options.HistoryMetaTTL) + if err != nil { + if errors.Is(err, ErrorUnrecoverablePosition) { + // Result contains stream position in case of ErrorUnrecoverablePosition + // during recovery. + latestOffset = historyResult.Offset + latestEpoch = historyResult.Epoch + res.Recovered = false + c.node.metrics.incRecover(res.Recovered) + } else { + c.node.logger.log(newLogEntry(LogLevelError, "error on recover", map[string]any{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()})) + return handleErr(err) + } + } else { latestOffset = historyResult.Offset latestEpoch = historyResult.Epoch - res.Recovered = false + var recovered bool + recoveredPubs, recovered = isStreamRecovered(historyResult, cmdOffset, cmdEpoch) + res.Recovered = recovered c.node.metrics.incRecover(res.Recovered) - } else { - c.node.logger.log(newLogEntry(LogLevelError, "error on recover", map[string]any{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()})) - c.pubSubSync.StopBuffering(channel) - if clientErr, ok := err.(*Error); ok && clientErr != ErrorInternal { - return errorDisconnectContext(clientErr, nil) - } - ctx.disconnect = &DisconnectServerError - return ctx } - } else { - latestOffset = historyResult.Offset - latestEpoch = historyResult.Epoch - var recovered bool - recoveredPubs, recovered = isRecovered(historyResult, cmdOffset, cmdEpoch) - res.Recovered = recovered - c.node.metrics.incRecover(res.Recovered) } } else { streamTop, err := c.node.streamTop(channel, reply.Options.HistoryMetaTTL) if err != nil { c.node.logger.log(newLogEntry(LogLevelError, "error getting stream state for channel", map[string]any{"channel": channel, "user": c.user, "client": c.uid, "error": err.Error()})) - c.pubSubSync.StopBuffering(channel) - if clientErr, ok := err.(*Error); ok && clientErr != ErrorInternal { - return errorDisconnectContext(clientErr, nil) - } - ctx.disconnect = &DisconnectServerError - return ctx + return handleErr(err) } latestOffset = streamTop.Offset latestEpoch = streamTop.Epoch @@ -2822,6 +2897,11 @@ func (c *Client) subscribeCmd(req *protocol.SubscribeRequest, reply SubscribeRep ctx.disconnect = &DisconnectInsufficientState return ctx } + if reply.Options.RecoveryMode == RecoveryModeCache && len(recoveredPubs) > 1 && req.Delta == "" { + // In RecoveryModeCache case client is only interested in last message. So if delta encoding is + // not used then we can only send the last publication. + recoveredPubs = recoveredPubs[len(recoveredPubs)-1:] + } } if len(recoveredPubs) > 0 { @@ -2835,14 +2915,22 @@ func (c *Client) subscribeCmd(req *protocol.SubscribeRequest, reply SubscribeRep } } + var channelFlags uint8 + if res.Recovered { // Only append recovered publications in case continuity in a channel can be achieved. - res.Publications = recoveredPubs - // In case of successful recovery attach stream position from request to subscribe response. + if res.Delta && req.Delta == string(DeltaTypeFossil) { + res.Publications = c.makeRecoveredPubsDeltaFossil(recoveredPubs) + // Allow delta for the following real-time publications since recovery is successful + // and makeRecoveredPubsDeltaFossil already created publication with base data if required. + channelFlags |= flagDeltaAllowed + } else { + res.Publications = recoveredPubs + } + // In case of successful recovery attach stream offset from request to subscribe response. // This simplifies client implementation as it doesn't need to distinguish between cases when // subscribe response has recovered publications, or it has no recovered publications. // Valid stream position will be then caught up upon processing publications. - res.Epoch = req.Epoch res.Offset = req.Offset } res.WasRecovering = req.Recover @@ -2867,7 +2955,6 @@ func (c *Client) subscribeCmd(req *protocol.SubscribeRequest, reply SubscribeRep defer c.handleCommandFinished(cmd, protocol.FrameTypeSubscribe, nil, protoReply, started) } - var channelFlags uint8 channelFlags |= flagSubscribed if serverSide { channelFlags |= flagServerSide @@ -2928,6 +3015,53 @@ func (c *Client) subscribeCmd(req *protocol.SubscribeRequest, reply SubscribeRep return ctx } +func (c *Client) makeRecoveredPubsDeltaFossil(recoveredPubs []*protocol.Publication) []*protocol.Publication { + if len(recoveredPubs) == 0 { + return nil + } + prevPub := recoveredPubs[0] + if c.transport.Protocol() == ProtocolTypeJSON { + // For JSON case we need to use JSON string (js) for data. + pub := &protocol.Publication{ + Offset: prevPub.Offset, + Info: prevPub.Info, + Tags: prevPub.Tags, + Data: json.Escape(convert.BytesToString(prevPub.Data)), + Delta: false, + } + recoveredPubs[0] = pub + } + // Probably during recovery we should not make deltas? This is something to investigate, in + // RecoveryModeCache case this won't be used since there is only one publication max recovered. + if len(recoveredPubs) > 1 { + for i, pub := range recoveredPubs[1:] { + patch := fdelta.Create(prevPub.Data, pub.Data) + var deltaPub *protocol.Publication + if c.transport.Protocol() == ProtocolTypeJSON { + // For JSON case we need to use JSON string (js) for patch. + deltaPub = &protocol.Publication{ + Offset: pub.Offset, + Data: json.Escape(convert.BytesToString(patch)), + Info: pub.Info, + Tags: pub.Tags, + Delta: true, + } + } else { + deltaPub = &protocol.Publication{ + Offset: pub.Offset, + Data: patch, + Info: pub.Info, + Tags: pub.Tags, + Delta: true, + } + } + recoveredPubs[i+1] = deltaPub + prevPub = recoveredPubs[i] + } + } + return recoveredPubs +} + func (c *Client) releaseSubscribeCommandReply(reply *protocol.Reply) { protocol.ReplyPool.ReleaseSubscribeReply(reply) } @@ -2965,38 +3099,50 @@ func (c *Client) handleAsyncUnsubscribe(ch string, unsub Unsubscribe) { } } -func (c *Client) writePublicationUpdatePosition(ch string, pub *protocol.Publication, data []byte, sp StreamPosition) error { +func (c *Client) writePublicationUpdatePosition(ch string, pub *protocol.Publication, prep preparedData, sp StreamPosition) error { c.mu.Lock() channelContext, ok := c.channels[ch] if !ok || !channelHasFlag(channelContext.flags, flagSubscribed) { c.mu.Unlock() return nil } + deltaAllowed := channelHasFlag(channelContext.flags, flagDeltaAllowed) if !channelHasFlag(channelContext.flags, flagPositioning) { + // Publication with Offset, but client does not use positioning. if hasFlag(c.transport.DisabledPushFlags(), PushFlagPublication) { c.mu.Unlock() return nil } c.mu.Unlock() - return c.transportEnqueue(data, ch, protocol.FrameTypePushPublication) + if pub.Offset == math.MaxUint64 { + // This is a special pub to trigger insufficient state. Noop in non-positioning case. + return nil + } + if prep.deltaSub { + if deltaAllowed { + return c.transportEnqueue(prep.localDeltaData, ch, protocol.FrameTypePushPublication) + } + c.mu.Lock() + if chCtx, chCtxOK := c.channels[ch]; chCtxOK { + chCtx.flags |= flagDeltaAllowed + c.channels[ch] = chCtx + } + c.mu.Unlock() + } + return c.transportEnqueue(prep.fullData, ch, protocol.FrameTypePushPublication) } serverSide := channelHasFlag(channelContext.flags, flagServerSide) currentPositionOffset := channelContext.streamPosition.Offset nextExpectedOffset := currentPositionOffset + 1 pubOffset := pub.Offset pubEpoch := sp.Epoch - if pubEpoch != channelContext.streamPosition.Epoch { + if pubEpoch != channelContext.streamPosition.Epoch || pubOffset != nextExpectedOffset { + // We can introduce an option to mark connection with insufficient state flag instead + // of disconnecting it immediately. In that case connection will eventually reconnect + // due to periodic sync. While connection channel is in the insufficient state we must + // skip publications coming to it. This mode may be useful to spread the resubscribe load. if c.node.logger.enabled(LogLevelDebug) { - c.node.logger.log(newLogEntry(LogLevelDebug, "client insufficient state", map[string]any{"channel": ch, "user": c.user, "client": c.uid, "epoch": pubEpoch, "expectedEpoch": channelContext.streamPosition.Epoch})) - } - // Oops: sth lost, let client reconnect/resubscribe to recover its state. - go func() { c.handleInsufficientState(ch, serverSide) }() - c.mu.Unlock() - return nil - } - if pubOffset != nextExpectedOffset { - if c.node.logger.enabled(LogLevelDebug) { - c.node.logger.log(newLogEntry(LogLevelDebug, "client insufficient state", map[string]any{"channel": ch, "user": c.user, "client": c.uid, "offset": pubOffset, "expectedOffset": nextExpectedOffset})) + c.node.logger.log(newLogEntry(LogLevelDebug, "client insufficient state", map[string]any{"channel": ch, "user": c.user, "client": c.uid, "epoch": pubEpoch, "expectedEpoch": channelContext.streamPosition.Epoch, "offset": pubOffset, "expectedOffset": nextExpectedOffset})) } // Oops: sth lost, let client reconnect/resubscribe to recover its state. go func() { c.handleInsufficientState(ch, serverSide) }() @@ -3010,10 +3156,25 @@ func (c *Client) writePublicationUpdatePosition(ch string, pub *protocol.Publica if hasFlag(c.transport.DisabledPushFlags(), PushFlagPublication) { return nil } - return c.transportEnqueue(data, ch, protocol.FrameTypePushPublication) + if prep.deltaSub { + if deltaAllowed { + return c.transportEnqueue(prep.brokerDeltaData, ch, protocol.FrameTypePushPublication) + } + c.mu.Lock() + if chCtx, chCtxOK := c.channels[ch]; chCtxOK { + chCtx.flags |= flagDeltaAllowed + c.channels[ch] = chCtx + } + c.mu.Unlock() + } + return c.transportEnqueue(prep.fullData, ch, protocol.FrameTypePushPublication) +} + +func (c *Client) writePublicationNoDelta(ch string, pub *protocol.Publication, data []byte, sp StreamPosition) error { + return c.writePublication(ch, pub, preparedData{fullData: data, brokerDeltaData: nil, localDeltaData: nil, deltaSub: false}, sp) } -func (c *Client) writePublication(ch string, pub *protocol.Publication, data []byte, sp StreamPosition) error { +func (c *Client) writePublication(ch string, pub *protocol.Publication, prep preparedData, sp StreamPosition) error { if c.node.LogEnabled(LogLevelTrace) { c.traceOutPush(&protocol.Push{Channel: ch, Pub: pub}) } @@ -3021,10 +3182,33 @@ func (c *Client) writePublication(ch string, pub *protocol.Publication, data []b if hasFlag(c.transport.DisabledPushFlags(), PushFlagPublication) { return nil } - return c.transportEnqueue(data, ch, protocol.FrameTypePushPublication) + + if prep.deltaSub { + // For this path (no Offset) delta may come from channel medium layer, so that we can use it + // here if allowed for the connection. + c.mu.RLock() + channelContext, ok := c.channels[ch] + if !ok { + c.mu.RUnlock() + return nil + } + deltaAllowed := channelHasFlag(channelContext.flags, flagDeltaAllowed) + c.mu.RUnlock() + + if deltaAllowed { + return c.transportEnqueue(prep.localDeltaData, ch, protocol.FrameTypePushPublication) + } + c.mu.Lock() + if chCtx, chCtxOK := c.channels[ch]; chCtxOK { + chCtx.flags |= flagDeltaAllowed + c.channels[ch] = chCtx + } + c.mu.Unlock() + } + return c.transportEnqueue(prep.fullData, ch, protocol.FrameTypePushPublication) } c.pubSubSync.SyncPublication(ch, pub, func() { - _ = c.writePublicationUpdatePosition(ch, pub, data, sp) + _ = c.writePublicationUpdatePosition(ch, pub, prep, sp) }) return nil } diff --git a/client_experimental.go b/client_experimental.go index 000c7f21..bfcb355c 100644 --- a/client_experimental.go +++ b/client_experimental.go @@ -29,7 +29,7 @@ func (c *Client) WritePublication(channel string, publication *Publication, sp S go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) return err } - return c.writePublication(channel, pub, jsonPush, sp) + return c.writePublicationNoDelta(channel, pub, jsonPush, sp) } else { push := &protocol.Push{Channel: channel, Pub: pub} var err error @@ -38,7 +38,7 @@ func (c *Client) WritePublication(channel string, publication *Publication, sp S go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) return err } - return c.writePublication(channel, pub, jsonReply, sp) + return c.writePublicationNoDelta(channel, pub, jsonReply, sp) } } else if protoType == protocol.TypeProtobuf { if c.transport.Unidirectional() { @@ -48,7 +48,7 @@ func (c *Client) WritePublication(channel string, publication *Publication, sp S if err != nil { return err } - return c.writePublication(channel, pub, protobufPush, sp) + return c.writePublicationNoDelta(channel, pub, protobufPush, sp) } else { push := &protocol.Push{Channel: channel, Pub: pub} var err error @@ -56,7 +56,7 @@ func (c *Client) WritePublication(channel string, publication *Publication, sp S if err != nil { return err } - return c.writePublication(channel, pub, protobufReply, sp) + return c.writePublicationNoDelta(channel, pub, protobufReply, sp) } } diff --git a/client_test.go b/client_test.go index 4abded52..2fce87e8 100644 --- a/client_test.go +++ b/client_test.go @@ -648,6 +648,46 @@ func TestClientSubscribeBrokerErrorOnRecoverHistory(t *testing.T) { } } +func TestClientSubscribeDeltaNotAllowed(t *testing.T) { + n := defaultTestNode() + defer func() { _ = n.Shutdown(context.Background()) }() + + ctx, cancelFn := context.WithCancel(context.Background()) + transport := newTestTransport(cancelFn) + transport.sink = make(chan []byte, 100) + transport.setProtocolType(ProtocolTypeJSON) + transport.setProtocolVersion(ProtocolVersion2) + client := newTestConnectedClientWithTransport(t, ctx, n, transport, "42") + rwWrapper := testReplyWriterWrapper() + err := client.handleSubscribe(&protocol.SubscribeRequest{ + Channel: "test_channel", + Delta: string(DeltaTypeFossil), + }, &protocol.Command{Id: 1}, time.Now(), rwWrapper.rw) + require.NoError(t, err) + require.Equal(t, 1, len(rwWrapper.replies)) + require.Nil(t, rwWrapper.replies[0].Error) + res := extractSubscribeResult(rwWrapper.replies) + require.False(t, res.Delta) +} + +func TestClientSubscribeUnknownDelta(t *testing.T) { + n := deltaTestNode() + defer func() { _ = n.Shutdown(context.Background()) }() + + ctx, cancelFn := context.WithCancel(context.Background()) + transport := newTestTransport(cancelFn) + transport.sink = make(chan []byte, 100) + transport.setProtocolType(ProtocolTypeJSON) + transport.setProtocolVersion(ProtocolVersion2) + client := newTestConnectedClientWithTransport(t, ctx, n, transport, "42") + rwWrapper := testReplyWriterWrapper() + err := client.handleSubscribe(&protocol.SubscribeRequest{ + Channel: "test_channel", + Delta: "invalid", + }, &protocol.Command{Id: 1}, time.Now(), rwWrapper.rw) + require.Equal(t, DisconnectBadRequest, err) +} + func testUnexpectedOffsetEpochProtocolV2(t *testing.T, offset uint64, epoch string) { t.Parallel() broker := NewTestBroker() @@ -676,9 +716,9 @@ func testUnexpectedOffsetEpochProtocolV2(t *testing.T, offset uint64, epoch stri }, &protocol.Command{}, time.Now(), rwWrapper.rw) require.NoError(t, err) - err = node.handlePublication("test", &Publication{ + err = node.handlePublication("test", StreamPosition{offset, epoch}, &Publication{ Offset: offset, - }, StreamPosition{offset, epoch}) + }, nil, nil) require.NoError(t, err) select { @@ -1503,7 +1543,7 @@ func TestClientPublishNotAvailable(t *testing.T) { type testBrokerEventHandler struct { // Publication must register callback func to handle Publications received. - HandlePublicationFunc func(ch string, pub *Publication, sp StreamPosition) error + HandlePublicationFunc func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error // Join must register callback func to handle Join messages received. HandleJoinFunc func(ch string, info *ClientInfo) error // Leave must register callback func to handle Leave messages received. @@ -1512,9 +1552,9 @@ type testBrokerEventHandler struct { HandleControlFunc func([]byte) error } -func (b *testBrokerEventHandler) HandlePublication(ch string, pub *Publication, sp StreamPosition) error { +func (b *testBrokerEventHandler) HandlePublication(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { if b.HandlePublicationFunc != nil { - return b.HandlePublicationFunc(ch, pub, sp) + return b.HandlePublicationFunc(ch, pub, sp, prevPub) } return nil } @@ -1560,7 +1600,7 @@ func TestClientPublishHandler(t *testing.T) { connectClientV2(t, client) node.broker.(*MemoryBroker).eventHandler = &testBrokerEventHandler{ - HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition) error { + HandlePublicationFunc: func(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { var msg testClientMessage err := json.Unmarshal(pub.Data, &msg) require.NoError(t, err) @@ -2995,7 +3035,7 @@ func TestClientCheckPosition(t *testing.T) { } node.mu.Unlock() - // no recover. + // no flagPositioning. got := client.checkPosition(300*time.Second, "channel", ChannelContext{}) require.True(t, got) @@ -3004,75 +3044,6 @@ func TestClientCheckPosition(t *testing.T) { require.True(t, got) } -func TestClientIsValidPosition(t *testing.T) { - node := defaultTestNode() - defer func() { _ = node.Shutdown(context.Background()) }() - - client := newTestClient(t, node, "42") - - node.mu.Lock() - node.nowTimeGetter = func() time.Time { - return time.Unix(200, 0) - } - node.mu.Unlock() - - client.channels = map[string]ChannelContext{ - "example": { - flags: flagSubscribed, - positionCheckTime: 50, - streamPosition: StreamPosition{ - Offset: 20, - Epoch: "test", - }, - }, - } - - got := client.isValidPosition(StreamPosition{ - Offset: 20, - Epoch: "test", - }, 200, "example") - require.True(t, got) - require.Equal(t, int64(200), client.channels["example"].positionCheckTime) - - got = client.isValidPosition(StreamPosition{ - Offset: 19, - Epoch: "test", - }, 210, "example") - require.True(t, got) - require.Equal(t, int64(210), client.channels["example"].positionCheckTime) - - got = client.isValidPosition(StreamPosition{ - Offset: 21, - Epoch: "test", - }, 220, "example") - require.False(t, got) - require.Equal(t, int64(210), client.channels["example"].positionCheckTime) - - client.channels = map[string]ChannelContext{ - "example": { - positionCheckTime: 50, - streamPosition: StreamPosition{ - Offset: 20, - Epoch: "test", - }, - }, - } - // no subscribed flag. - got = client.isValidPosition(StreamPosition{ - Offset: 21, - Epoch: "test", - }, 220, "example") - require.True(t, got) - - _ = client.close(DisconnectConnectionClosed) - // closed client. - got = client.isValidPosition(StreamPosition{ - Offset: 21, - Epoch: "test", - }, 220, "example") - require.True(t, got) -} - func TestErrLogLevel(t *testing.T) { require.Equal(t, LogLevelInfo, errLogLevel(ErrorNotAvailable)) require.Equal(t, LogLevelError, errLogLevel(errors.New("boom"))) @@ -3080,7 +3051,7 @@ func TestErrLogLevel(t *testing.T) { func errLogLevel(err error) LogLevel { logLevel := LogLevelInfo - if err != ErrorNotAvailable { + if !errors.Is(err, ErrorNotAvailable) { logLevel = LogLevelError } return logLevel diff --git a/config.go b/config.go index 5de49aab..8bf247b1 100644 --- a/config.go +++ b/config.go @@ -108,6 +108,12 @@ type Config struct { // function for extracting channel_namespace label for transport_messages_received and // transport_messages_received_size. ChannelNamespaceLabelForTransportMessagesReceived bool + + // GetChannelMediumOptions is a way to provide ChannelMediumOptions for specific channel. + // This function is called each time new channel appears on the Node. If it returns false + // then no medium layer will be used for the channel. + // See the doc comment for ChannelMediumOptions for more details about channel medium concept. + GetChannelMediumOptions func(channel string) (ChannelMediumOptions, bool) } const ( diff --git a/events.go b/events.go index 7e88b9fb..0fe10e98 100644 --- a/events.go +++ b/events.go @@ -358,6 +358,25 @@ type HistoryHandler func(HistoryEvent, HistoryCallback) // internal state. Returning a copy is important to avoid data races. type StateSnapshotHandler func() (any, error) +// CacheEmptyEvent is issued when recovery mode is used but Centrifuge can't +// find Publication in history to recover from. This event allows application +// to decide what to do in this case – it's possible to populate the cache by +// sending actual data to a channel. +type CacheEmptyEvent struct { + Channel string +} + +// CacheEmptyReply contains fields determining the reaction on cache empty event. +type CacheEmptyReply struct { + // Populated when set to true tells Centrifuge that cache was populated and + // in that case Centrifuge will try to recover missed Publication from history + // one more time. + Populated bool +} + +// CacheEmptyHandler allows setting cache empty handler function. +type CacheEmptyHandler func(CacheEmptyEvent) (CacheEmptyReply, error) + // SurveyEvent with Op and Data of survey. type SurveyEvent struct { Op string diff --git a/go.mod b/go.mod index 15009aa1..f0b13626 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,12 @@ go 1.21 require ( github.com/FZambia/eagle v0.1.0 github.com/Yiling-J/theine-go v0.3.2 - github.com/centrifugal/protocol v0.12.1 + github.com/centrifugal/protocol v0.13.0 github.com/google/uuid v1.6.0 github.com/prometheus/client_golang v1.19.1 - github.com/redis/rueidis v1.0.37-0.20240510165047-ebd66b7de128 + github.com/redis/rueidis v1.0.37 github.com/segmentio/encoding v0.4.0 + github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b github.com/stretchr/testify v1.9.0 golang.org/x/sync v0.7.0 google.golang.org/protobuf v1.34.1 diff --git a/go.sum b/go.sum index c0de5171..23b065e4 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Yiling-J/theine-go v0.3.2 h1:XcSdMPV9DwBD9gqqSxbBfVJnP8CCiqNSqp3C6Ypm github.com/Yiling-J/theine-go v0.3.2/go.mod h1:ygLXqrWPZT/a+PzK5hQ0+a6gu0lpAY5IudTcgnPleqI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/centrifugal/protocol v0.12.1 h1:hGbIl9Y0UbVsESgLcsqgZ7duwEnrZebFUYdu5Opwzgo= -github.com/centrifugal/protocol v0.12.1/go.mod h1:5Z0SuNdXEt83Fkoi34BCyY23p1P8+zQakQS6/BfJHak= +github.com/centrifugal/protocol v0.13.0 h1:3j9CWlbML5O9OlhLmSPWgptby0hDn4pQC9W+q6UiQQo= +github.com/centrifugal/protocol v0.13.0/go.mod h1:lM54PGU/u5WupYSb755Zv6tZ2ju1SqNKCp6A4s0DeG4= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -38,14 +38,16 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/redis/rueidis v1.0.37-0.20240510165047-ebd66b7de128 h1:wjJwFyRE8EYJVASGhwnQ7oI2uPdRrCGuPKiI8kpgGLE= -github.com/redis/rueidis v1.0.37-0.20240510165047-ebd66b7de128/go.mod h1:bnbkk4+CkXZgDPEbUtSos/o55i4RhFYYesJ4DS2zmq0= +github.com/redis/rueidis v1.0.37 h1:RBb1s97wcvlK94YZvyh+B/c6zOkc0ssamlfWRGfRlaw= +github.com/redis/rueidis v1.0.37/go.mod h1:bnbkk4+CkXZgDPEbUtSos/o55i4RhFYYesJ4DS2zmq0= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.4.0 h1:MEBYvRqiUB2nfR2criEXWqwdY6HJOUrCn5hboVOVmy8= github.com/segmentio/encoding v0.4.0/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= +github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b h1:SCYeryKXBVdW38167VyumGakH+7E4Wxe6b/zxmQxwyM= +github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b/go.mod h1:daNLfX/GJKuZyN4HkMf0h8dVmTmgRbBSkd9bFQyGNIo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/hub.go b/hub.go index 7759b1d9..d0f7c067 100644 --- a/hub.go +++ b/hub.go @@ -5,7 +5,11 @@ import ( "io" "sync" + "github.com/centrifugal/centrifuge/internal/convert" + "github.com/centrifugal/protocol" + "github.com/segmentio/encoding/json" + fdelta "github.com/shadowspore/fossil-delta" ) const numHubShards = 64 @@ -86,15 +90,15 @@ func (h *Hub) remove(c *Client) error { // Connections returns all user connections to the current Node. func (h *Hub) Connections() map[string]*Client { - conns := make(map[string]*Client) + connections := make(map[string]*Client) for _, shard := range h.connShards { shard.mu.RLock() - for clientID, c := range shard.conns { - conns[clientID] = c + for clientID, c := range shard.clients { + connections[clientID] = c } shard.mu.RUnlock() } - return conns + return connections } // UserConnections returns all user connections to the current Node. @@ -118,8 +122,8 @@ func (h *Hub) disconnect(userID string, disconnect Disconnect, clientID, session return h.connShards[index(userID, numHubShards)].disconnect(userID, disconnect, clientID, sessionID, whitelist) } -func (h *Hub) addSub(ch string, c *Client) (bool, error) { - return h.subShards[index(ch, numHubShards)].addSub(ch, c) +func (h *Hub) addSub(ch string, sub subInfo) (bool, error) { + return h.subShards[index(ch, numHubShards)].addSub(ch, sub) } // removeSub removes connection from clientHub subscriptions registry. @@ -131,9 +135,13 @@ func (h *Hub) removeSub(ch string, c *Client) (bool, error) { // Usually this is NOT what you need since in most cases you should use Node.Publish method which // uses a Broker to deliver publications to all Nodes in a cluster and maintains publication history // in a channel with incremental offset. By calling BroadcastPublication messages will only be sent -// to the current node subscribers without any defined offset semantics. +// to the current node subscribers without any defined offset semantics, without delta support. func (h *Hub) BroadcastPublication(ch string, pub *Publication, sp StreamPosition) error { - return h.subShards[index(ch, numHubShards)].broadcastPublication(ch, pubToProto(pub), sp) + return h.broadcastPublication(ch, sp, pub, nil, nil) +} + +func (h *Hub) broadcastPublication(ch string, sp StreamPosition, pub, prevPub, localPrevPub *Publication) error { + return h.subShards[index(ch, numHubShards)].broadcastPublication(ch, sp, pub, prevPub, localPrevPub) } // broadcastJoin sends message to all clients subscribed on channel. @@ -201,15 +209,15 @@ func (h *Hub) NumChannels() int { type connShard struct { mu sync.RWMutex // match client ID with actual client connection. - conns map[string]*Client + clients map[string]*Client // registry to hold active client connections grouped by user. users map[string]map[string]struct{} } func newConnShard() *connShard { return &connShard{ - conns: make(map[string]*Client), - users: make(map[string]map[string]struct{}), + clients: make(map[string]*Client), + users: make(map[string]map[string]struct{}), } } @@ -225,8 +233,8 @@ func (h *connShard) shutdown(ctx context.Context, sem chan struct{}) error { h.mu.RLock() // At this moment node won't accept new client connections, so we can // safely copy existing clients and release lock. - clients := make([]*Client, 0, len(h.conns)) - for _, client := range h.conns { + clients := make([]*Client, 0, len(h.clients)) + for _, client := range h.clients { clients = append(clients, client) } h.mu.RUnlock() @@ -394,16 +402,16 @@ func (h *connShard) userConnections(userID string) map[string]*Client { return map[string]*Client{} } - conns := make(map[string]*Client, len(userConnections)) + connections := make(map[string]*Client, len(userConnections)) for uid := range userConnections { - c, ok := h.conns[uid] + c, ok := h.clients[uid] if !ok { continue } - conns[uid] = c + connections[uid] = c } - return conns + return connections } // Add connection into clientHub connections registry. @@ -414,7 +422,7 @@ func (h *connShard) add(c *Client) error { uid := c.ID() user := c.UserID() - h.conns[uid] = c + h.clients[uid] = c if _, ok := h.users[user]; !ok { h.users[user] = make(map[string]struct{}) @@ -431,7 +439,7 @@ func (h *connShard) remove(c *Client) error { uid := c.ID() user := c.UserID() - delete(h.conns, uid) + delete(h.clients, uid) // try to find connection to delete, return early if not found. if _, ok := h.users[user]; !ok { @@ -470,32 +478,49 @@ func (h *connShard) NumUsers() int { return len(h.users) } +type DeltaType string + +const ( + deltaTypeNone DeltaType = "" + // DeltaTypeFossil is Fossil delta encoding. See https://fossil-scm.org/home/doc/tip/www/delta_encoder_algorithm.wiki. + DeltaTypeFossil DeltaType = "fossil" +) + +var stringToDeltaType = map[string]DeltaType{ + "fossil": DeltaTypeFossil, +} + +type subInfo struct { + client *Client + deltaType DeltaType +} + type subShard struct { mu sync.RWMutex - // registry to hold active subscriptions of clients to channels. - subs map[string]map[string]*Client + // registry to hold active subscriptions of clients to channels with some additional info. + subs map[string]map[string]subInfo logger *logger } func newSubShard(logger *logger) *subShard { return &subShard{ - subs: make(map[string]map[string]*Client), + subs: make(map[string]map[string]subInfo), logger: logger, } } // addSub adds connection into clientHub subscriptions registry. -func (h *subShard) addSub(ch string, c *Client) (bool, error) { +func (h *subShard) addSub(ch string, sub subInfo) (bool, error) { h.mu.Lock() defer h.mu.Unlock() - uid := c.ID() + uid := sub.client.ID() _, ok := h.subs[ch] if !ok { - h.subs[ch] = make(map[string]*Client) + h.subs[ch] = make(map[string]subInfo) } - h.subs[ch][uid] = c + h.subs[ch][uid] = sub if !ok { return true, nil } @@ -535,8 +560,95 @@ type encodeError struct { error error } -// broadcastPublication sends message to all clients subscribed on channel. -func (h *subShard) broadcastPublication(channel string, pub *protocol.Publication, sp StreamPosition) error { +type preparedKey struct { + ProtocolType protocol.Type + Unidirectional bool + DeltaType DeltaType +} + +type preparedData struct { + fullData []byte + brokerDeltaData []byte + localDeltaData []byte + deltaSub bool +} + +func getDeltaPub(prevPub *Publication, fullPub *protocol.Publication, key preparedKey) *protocol.Publication { + deltaPub := fullPub + if prevPub != nil && key.DeltaType == DeltaTypeFossil { + patch := fdelta.Create(prevPub.Data, fullPub.Data) + if key.ProtocolType == protocol.TypeJSON { + deltaPub = &protocol.Publication{ + Offset: fullPub.Offset, + Data: json.Escape(convert.BytesToString(patch)), + Info: fullPub.Info, + Tags: fullPub.Tags, + Delta: true, + } + } else { + deltaPub = &protocol.Publication{ + Offset: fullPub.Offset, + Data: patch, + Info: fullPub.Info, + Tags: fullPub.Tags, + Delta: true, + } + } + } else if prevPub == nil && key.ProtocolType == protocol.TypeJSON && key.DeltaType == DeltaTypeFossil { + // In JSON and Fossil case we need to send full state in JSON string format. + deltaPub = &protocol.Publication{ + Offset: fullPub.Offset, + Data: json.Escape(convert.BytesToString(fullPub.Data)), + Info: fullPub.Info, + Tags: fullPub.Tags, + } + } + return deltaPub +} + +func getDeltaData(sub subInfo, key preparedKey, channel string, deltaPub *protocol.Publication, jsonEncodeErr *encodeError) ([]byte, error) { + var deltaData []byte + if key.ProtocolType == protocol.TypeJSON { + if sub.client.transport.Unidirectional() { + push := &protocol.Push{Channel: channel, Pub: deltaPub} + var err error + deltaData, err = protocol.DefaultJsonPushEncoder.Encode(push) + if err != nil { + *jsonEncodeErr = encodeError{client: sub.client.ID(), user: sub.client.UserID(), error: err} + } + } else { + push := &protocol.Push{Channel: channel, Pub: deltaPub} + var err error + deltaData, err = protocol.DefaultJsonReplyEncoder.Encode(&protocol.Reply{Push: push}) + if err != nil { + *jsonEncodeErr = encodeError{client: sub.client.ID(), user: sub.client.UserID(), error: err} + } + } + } else if key.ProtocolType == protocol.TypeProtobuf { + if sub.client.transport.Unidirectional() { + push := &protocol.Push{Channel: channel, Pub: deltaPub} + var err error + deltaData, err = protocol.DefaultProtobufPushEncoder.Encode(push) + if err != nil { + return nil, err + } + } else { + push := &protocol.Push{Channel: channel, Pub: deltaPub} + var err error + deltaData, err = protocol.DefaultProtobufReplyEncoder.Encode(&protocol.Reply{Push: push}) + if err != nil { + return nil, err + } + } + } + return deltaData, nil +} + +// broadcastPublication sends message to all clients subscribed on a channel. +func (h *subShard) broadcastPublication(channel string, sp StreamPosition, pub, prevPub, localPrevPub *Publication) error { + fullPub := pubToProto(pub) + preparedDataByKey := make(map[preparedKey]preparedData) + h.mu.RLock() defer h.mu.RUnlock() @@ -546,70 +658,104 @@ func (h *subShard) broadcastPublication(channel string, pub *protocol.Publicatio } var ( - jsonReply []byte - protobufReply []byte - - jsonPush []byte - protobufPush []byte - jsonEncodeErr *encodeError ) - for _, c := range channelSubscribers { - protoType := c.Transport().Protocol().toProto() - if protoType == protocol.TypeJSON { - if jsonEncodeErr != nil { - go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) - continue + for _, sub := range channelSubscribers { + key := preparedKey{ + ProtocolType: sub.client.Transport().Protocol().toProto(), + Unidirectional: sub.client.transport.Unidirectional(), + DeltaType: sub.deltaType, + } + prepValue, prepDataFound := preparedDataByKey[key] + if !prepDataFound { + var brokerDeltaPub *protocol.Publication + if fullPub.Offset > 0 { + brokerDeltaPub = getDeltaPub(prevPub, fullPub, key) } - if c.transport.Unidirectional() { - if jsonPush == nil { - push := &protocol.Push{Channel: channel, Pub: pub} + localDeltaPub := getDeltaPub(localPrevPub, fullPub, key) + + var brokerDeltaData []byte + var localDeltaData []byte + if key.DeltaType != deltaTypeNone { + var err error + brokerDeltaData, err = getDeltaData(sub, key, channel, brokerDeltaPub, jsonEncodeErr) + if err != nil { + return err + } + localDeltaData, err = getDeltaData(sub, key, channel, localDeltaPub, jsonEncodeErr) + if err != nil { + return err + } + } + + var fullData []byte + + if key.ProtocolType == protocol.TypeJSON { + if sub.client.transport.Unidirectional() { + pubToUse := fullPub + if key.ProtocolType == protocol.TypeJSON && key.DeltaType == DeltaTypeFossil { + pubToUse = &protocol.Publication{ + Offset: fullPub.Offset, + Data: json.Escape(convert.BytesToString(fullPub.Data)), + Info: fullPub.Info, + Tags: fullPub.Tags, + } + } + push := &protocol.Push{Channel: channel, Pub: pubToUse} var err error - jsonPush, err = protocol.DefaultJsonPushEncoder.Encode(push) + fullData, err = protocol.DefaultJsonPushEncoder.Encode(push) if err != nil { - jsonEncodeErr = &encodeError{client: c.ID(), user: c.UserID(), error: err} - go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) - continue + jsonEncodeErr = &encodeError{client: sub.client.ID(), user: sub.client.UserID(), error: err} } - } - _ = c.writePublication(channel, pub, jsonPush, sp) - } else { - if jsonReply == nil { - push := &protocol.Push{Channel: channel, Pub: pub} + } else { + pubToUse := fullPub + if key.ProtocolType == protocol.TypeJSON && key.DeltaType == DeltaTypeFossil { + pubToUse = &protocol.Publication{ + Offset: fullPub.Offset, + Data: json.Escape(convert.BytesToString(fullPub.Data)), + Info: fullPub.Info, + Tags: fullPub.Tags, + } + } + push := &protocol.Push{Channel: channel, Pub: pubToUse} var err error - jsonReply, err = protocol.DefaultJsonReplyEncoder.Encode(&protocol.Reply{Push: push}) + fullData, err = protocol.DefaultJsonReplyEncoder.Encode(&protocol.Reply{Push: push}) if err != nil { - jsonEncodeErr = &encodeError{client: c.ID(), user: c.UserID(), error: err} - go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) - continue + jsonEncodeErr = &encodeError{client: sub.client.ID(), user: sub.client.UserID(), error: err} } } - _ = c.writePublication(channel, pub, jsonReply, sp) - } - } else if protoType == protocol.TypeProtobuf { - if c.transport.Unidirectional() { - if protobufPush == nil { - push := &protocol.Push{Channel: channel, Pub: pub} + } else if key.ProtocolType == protocol.TypeProtobuf { + if sub.client.transport.Unidirectional() { + push := &protocol.Push{Channel: channel, Pub: fullPub} var err error - protobufPush, err = protocol.DefaultProtobufPushEncoder.Encode(push) + fullData, err = protocol.DefaultProtobufPushEncoder.Encode(push) if err != nil { return err } - } - _ = c.writePublication(channel, pub, protobufPush, sp) - } else { - if protobufReply == nil { - push := &protocol.Push{Channel: channel, Pub: pub} + } else { + push := &protocol.Push{Channel: channel, Pub: fullPub} var err error - protobufReply, err = protocol.DefaultProtobufReplyEncoder.Encode(&protocol.Reply{Push: push}) + fullData, err = protocol.DefaultProtobufReplyEncoder.Encode(&protocol.Reply{Push: push}) if err != nil { return err } } - _ = c.writePublication(channel, pub, protobufReply, sp) } + + prepValue = preparedData{ + fullData: fullData, + brokerDeltaData: brokerDeltaData, + localDeltaData: localDeltaData, + deltaSub: key.DeltaType != deltaTypeNone, + } + preparedDataByKey[key] = prepValue + } + if sub.client.transport.Protocol() == ProtocolTypeJSON && jsonEncodeErr != nil { + go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(sub.client) + continue } + _ = sub.client.writePublication(channel, fullPub, prepValue, sp) } if jsonEncodeErr != nil && h.logger.enabled(LogLevelWarn) { // Log that we had clients with inappropriate protocol, and point to the first such client. @@ -643,40 +789,40 @@ func (h *subShard) broadcastJoin(channel string, join *protocol.Join) error { jsonEncodeErr *encodeError ) - for _, c := range channelSubscribers { - protoType := c.Transport().Protocol().toProto() + for _, sub := range channelSubscribers { + protoType := sub.client.Transport().Protocol().toProto() if protoType == protocol.TypeJSON { if jsonEncodeErr != nil { - go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) + go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(sub.client) continue } - if c.transport.Unidirectional() { + if sub.client.transport.Unidirectional() { if jsonPush == nil { push := &protocol.Push{Channel: channel, Join: join} var err error jsonPush, err = protocol.DefaultJsonPushEncoder.Encode(push) if err != nil { - jsonEncodeErr = &encodeError{client: c.ID(), user: c.UserID(), error: err} - go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) + jsonEncodeErr = &encodeError{client: sub.client.ID(), user: sub.client.UserID(), error: err} + go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(sub.client) continue } } - _ = c.writeJoin(channel, join, jsonPush) + _ = sub.client.writeJoin(channel, join, jsonPush) } else { if jsonReply == nil { push := &protocol.Push{Channel: channel, Join: join} var err error jsonReply, err = protocol.DefaultJsonReplyEncoder.Encode(&protocol.Reply{Push: push}) if err != nil { - jsonEncodeErr = &encodeError{client: c.ID(), user: c.UserID(), error: err} - go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) + jsonEncodeErr = &encodeError{client: sub.client.ID(), user: sub.client.UserID(), error: err} + go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(sub.client) continue } } - _ = c.writeJoin(channel, join, jsonReply) + _ = sub.client.writeJoin(channel, join, jsonReply) } } else if protoType == protocol.TypeProtobuf { - if c.transport.Unidirectional() { + if sub.client.transport.Unidirectional() { if protobufPush == nil { push := &protocol.Push{Channel: channel, Join: join} var err error @@ -685,7 +831,7 @@ func (h *subShard) broadcastJoin(channel string, join *protocol.Join) error { return err } } - _ = c.writeJoin(channel, join, protobufPush) + _ = sub.client.writeJoin(channel, join, protobufPush) } else { if protobufReply == nil { push := &protocol.Push{Channel: channel, Join: join} @@ -695,7 +841,7 @@ func (h *subShard) broadcastJoin(channel string, join *protocol.Join) error { return err } } - _ = c.writeJoin(channel, join, protobufReply) + _ = sub.client.writeJoin(channel, join, protobufReply) } } } @@ -731,40 +877,40 @@ func (h *subShard) broadcastLeave(channel string, leave *protocol.Leave) error { jsonEncodeErr *encodeError ) - for _, c := range channelSubscribers { - protoType := c.Transport().Protocol().toProto() + for _, sub := range channelSubscribers { + protoType := sub.client.Transport().Protocol().toProto() if protoType == protocol.TypeJSON { if jsonEncodeErr != nil { - go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) + go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(sub.client) continue } - if c.transport.Unidirectional() { + if sub.client.transport.Unidirectional() { if jsonPush == nil { push := &protocol.Push{Channel: channel, Leave: leave} var err error jsonPush, err = protocol.DefaultJsonPushEncoder.Encode(push) if err != nil { - jsonEncodeErr = &encodeError{client: c.ID(), user: c.UserID(), error: err} - go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) + jsonEncodeErr = &encodeError{client: sub.client.ID(), user: sub.client.UserID(), error: err} + go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(sub.client) continue } } - _ = c.writeLeave(channel, leave, jsonPush) + _ = sub.client.writeLeave(channel, leave, jsonPush) } else { if jsonReply == nil { push := &protocol.Push{Channel: channel, Leave: leave} var err error jsonReply, err = protocol.DefaultJsonReplyEncoder.Encode(&protocol.Reply{Push: push}) if err != nil { - jsonEncodeErr = &encodeError{client: c.ID(), user: c.UserID(), error: err} - go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(c) + jsonEncodeErr = &encodeError{client: sub.client.ID(), user: sub.client.UserID(), error: err} + go func(c *Client) { c.Disconnect(DisconnectInappropriateProtocol) }(sub.client) continue } } - _ = c.writeLeave(channel, leave, jsonReply) + _ = sub.client.writeLeave(channel, leave, jsonReply) } } else if protoType == protocol.TypeProtobuf { - if c.transport.Unidirectional() { + if sub.client.transport.Unidirectional() { if protobufPush == nil { push := &protocol.Push{Channel: channel, Leave: leave} var err error @@ -773,7 +919,7 @@ func (h *subShard) broadcastLeave(channel string, leave *protocol.Leave) error { return err } } - _ = c.writeLeave(channel, leave, protobufPush) + _ = sub.client.writeLeave(channel, leave, protobufPush) } else { if protobufReply == nil { push := &protocol.Push{Channel: channel, Leave: leave} @@ -783,7 +929,7 @@ func (h *subShard) broadcastLeave(channel string, leave *protocol.Leave) error { return err } } - _ = c.writeLeave(channel, leave, protobufReply) + _ = sub.client.writeLeave(channel, leave, protobufReply) } } } @@ -834,9 +980,9 @@ func (h *subShard) Channels() []string { func (h *subShard) NumSubscribers(ch string) int { h.mu.RLock() defer h.mu.RUnlock() - conns, ok := h.subs[ch] + clients, ok := h.subs[ch] if !ok { return 0 } - return len(conns) + return len(clients) } diff --git a/hub_test.go b/hub_test.go index 56d45c94..d06e9f67 100644 --- a/hub_test.go +++ b/hub_test.go @@ -10,6 +10,11 @@ import ( "testing" "time" + "github.com/centrifugal/centrifuge/internal/convert" + + "github.com/centrifugal/protocol" + "github.com/segmentio/encoding/json" + fdelta "github.com/shadowspore/fossil-delta" "github.com/stretchr/testify/require" ) @@ -444,10 +449,10 @@ func TestHubBroadcastPublication(t *testing.T) { protocolVersion ProtocolVersion uni bool }{ - {name: "JSON-V2", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2}, - {name: "Protobuf-V2", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2}, - {name: "JSON-V2-uni", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2, uni: true}, - {name: "Protobuf-V2-uni", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2, uni: true}, + {name: "JSON", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2}, + {name: "Protobuf", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2}, + {name: "JSON-uni", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2, uni: true}, + {name: "Protobuf-uni", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2, uni: true}, } for _, tc := range tcs { @@ -496,6 +501,298 @@ func TestHubBroadcastPublication(t *testing.T) { } } +func deltaTestNode() *Node { + n := defaultNodeNoHandlers() + n.OnConnect(func(client *Client) { + client.OnSubscribe(func(e SubscribeEvent, cb SubscribeCallback) { + cb(SubscribeReply{ + Options: SubscribeOptions{ + EnableRecovery: true, + RecoveryMode: RecoveryModeCache, + AllowedDeltaTypes: []DeltaType{DeltaTypeFossil}, + }, + }, nil) + }) + client.OnPublish(func(e PublishEvent, cb PublishCallback) { + cb(PublishReply{}, nil) + }) + }) + return n +} + +func deltaTestNodeNoRecovery() *Node { + n := defaultNodeNoHandlers() + n.OnConnect(func(client *Client) { + client.OnSubscribe(func(e SubscribeEvent, cb SubscribeCallback) { + cb(SubscribeReply{ + Options: SubscribeOptions{ + AllowedDeltaTypes: []DeltaType{DeltaTypeFossil}, + }, + }, nil) + }) + client.OnPublish(func(e PublishEvent, cb PublishCallback) { + cb(PublishReply{}, nil) + }) + }) + return n +} + +func newTestSubscribedClientWithTransportDelta(t *testing.T, ctx context.Context, n *Node, transport Transport, userID, chanID string, deltaType DeltaType) *Client { + client := newTestConnectedClientWithTransport(t, ctx, n, transport, userID) + subscribeClientDelta(t, client, chanID, deltaType) + require.True(t, n.hub.NumSubscribers(chanID) > 0) + require.Contains(t, client.channels, chanID) + return client +} + +func subscribeClientDelta(t testing.TB, client *Client, ch string, deltaType DeltaType) *protocol.SubscribeResult { + rwWrapper := testReplyWriterWrapper() + err := client.handleSubscribe(&protocol.SubscribeRequest{ + Channel: ch, + Delta: string(deltaType), + }, &protocol.Command{Id: 1}, time.Now(), rwWrapper.rw) + require.NoError(t, err) + require.Nil(t, rwWrapper.replies[0].Error) + return rwWrapper.replies[0].Subscribe +} + +func TestHubBroadcastPublicationDelta(t *testing.T) { + tcs := []struct { + name string + protocolType ProtocolType + protocolVersion ProtocolVersion + uni bool + }{ + {name: "JSON", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2}, + {name: "Protobuf", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2}, + {name: "JSON-uni", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2, uni: true}, + {name: "Protobuf-uni", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2, uni: true}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + n := deltaTestNode() + n.config.GetChannelNamespaceLabel = func(channel string) string { + return channel + } + defer func() { _ = n.Shutdown(context.Background()) }() + + ctx, cancelFn := context.WithCancel(context.Background()) + transport := newTestTransport(cancelFn) + transport.sink = make(chan []byte, 100) + transport.setProtocolType(tc.protocolType) + transport.setProtocolVersion(tc.protocolVersion) + transport.setUnidirectional(tc.uni) + newTestSubscribedClientWithTransportDelta( + t, ctx, n, transport, "42", "test_channel", DeltaTypeFossil) + + res, err := n.History("test_channel") + require.NoError(t, err) + + err = n.hub.broadcastPublication( + "test_channel", + StreamPosition{Offset: 1, Epoch: res.StreamPosition.Epoch}, + &Publication{Data: []byte(`{"data": "broadcast_data"}`), Offset: 1}, + nil, + nil, + ) + require.NoError(t, err) + + LOOP: + for { + select { + case data := <-transport.sink: + if strings.Contains(string(data), "broadcast_data") { + break LOOP + } + case <-time.After(2 * time.Second): + t.Fatal("no data in sink") + } + } + + // Broadcast same data to existing channel. + err = n.hub.broadcastPublication( + "test_channel", + StreamPosition{Offset: 2, Epoch: res.StreamPosition.Epoch}, + &Publication{Data: []byte(`{"data": "broadcast_data"}`), Offset: 2}, + &Publication{Data: []byte(`{"data": "broadcast_data"}`), Offset: 1}, + nil, + ) + require.NoError(t, err) + + LOOP2: + for { + select { + case data := <-transport.sink: + if strings.Contains(string(data), "broadcast_data") { + require.Fail(t, "should not receive same data twice - delta expected") + } + break LOOP2 + case <-time.After(2 * time.Second): + t.Fatal("no data in sink 2") + } + } + }) + } +} + +func TestHubBroadcastPublicationDeltaAtMostOnce(t *testing.T) { + tcs := []struct { + name string + protocolType ProtocolType + protocolVersion ProtocolVersion + uni bool + }{ + {name: "JSON", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2}, + {name: "Protobuf", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2}, + {name: "JSON-uni", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2, uni: true}, + {name: "Protobuf-uni", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2, uni: true}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + n := deltaTestNodeNoRecovery() + n.config.GetChannelNamespaceLabel = func(channel string) string { + return channel + } + defer func() { _ = n.Shutdown(context.Background()) }() + + ctx, cancelFn := context.WithCancel(context.Background()) + transport := newTestTransport(cancelFn) + transport.sink = make(chan []byte, 100) + transport.setProtocolType(tc.protocolType) + transport.setProtocolVersion(tc.protocolVersion) + transport.setUnidirectional(tc.uni) + newTestSubscribedClientWithTransportDelta( + t, ctx, n, transport, "42", "test_channel", DeltaTypeFossil) + + res, err := n.History("test_channel") + require.NoError(t, err) + + err = n.hub.broadcastPublication( + "test_channel", + StreamPosition{Offset: 1, Epoch: res.StreamPosition.Epoch}, + &Publication{Data: []byte(`{"data": "broadcast_data"}`), Offset: 1}, + nil, + nil, + ) + require.NoError(t, err) + + LOOP: + for { + select { + case data := <-transport.sink: + if strings.Contains(string(data), "broadcast_data") { + break LOOP + } + case <-time.After(2 * time.Second): + t.Fatal("no data in sink") + } + } + + // Broadcast same data to existing channel. + err = n.hub.broadcastPublication( + "test_channel", + StreamPosition{Offset: 2, Epoch: res.StreamPosition.Epoch}, + &Publication{Data: []byte(`{"data": "broadcast_data"}`), Offset: 2}, + nil, + &Publication{Data: []byte(`{"data": "broadcast_data"}`), Offset: 1}, + ) + require.NoError(t, err) + + LOOP2: + for { + select { + case data := <-transport.sink: + if strings.Contains(string(data), "broadcast_data") { + require.Fail(t, "should not receive same data twice - delta expected") + } + break LOOP2 + case <-time.After(2 * time.Second): + t.Fatal("no data in sink 2") + } + } + }) + } +} + +func TestHubBroadcastPublicationDeltaAtMostOnceNoOffset(t *testing.T) { + tcs := []struct { + name string + protocolType ProtocolType + protocolVersion ProtocolVersion + uni bool + }{ + {name: "JSON", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2}, + {name: "Protobuf", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2}, + {name: "JSON-uni", protocolType: ProtocolTypeJSON, protocolVersion: ProtocolVersion2, uni: true}, + {name: "Protobuf-uni", protocolType: ProtocolTypeProtobuf, protocolVersion: ProtocolVersion2, uni: true}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + n := deltaTestNodeNoRecovery() + n.config.GetChannelNamespaceLabel = func(channel string) string { + return channel + } + defer func() { _ = n.Shutdown(context.Background()) }() + + ctx, cancelFn := context.WithCancel(context.Background()) + transport := newTestTransport(cancelFn) + transport.sink = make(chan []byte, 100) + transport.setProtocolType(tc.protocolType) + transport.setProtocolVersion(tc.protocolVersion) + transport.setUnidirectional(tc.uni) + newTestSubscribedClientWithTransportDelta( + t, ctx, n, transport, "42", "test_channel", DeltaTypeFossil) + + err := n.hub.broadcastPublication( + "test_channel", + StreamPosition{}, + &Publication{Data: []byte(`{"data": "broadcast_data"}`)}, + nil, + nil, + ) + require.NoError(t, err) + + LOOP: + for { + select { + case data := <-transport.sink: + if strings.Contains(string(data), "broadcast_data") { + break LOOP + } + case <-time.After(2 * time.Second): + t.Fatal("no data in sink") + } + } + + // Broadcast same data to existing channel. + err = n.hub.broadcastPublication( + "test_channel", + StreamPosition{}, + &Publication{Data: []byte(`{"data": "broadcast_data"}`)}, + nil, + &Publication{Data: []byte(`{"data": "broadcast_data"}`)}, + ) + require.NoError(t, err) + + LOOP2: + for { + select { + case data := <-transport.sink: + if strings.Contains(string(data), "broadcast_data") { + require.Fail(t, "should not receive same data twice - delta expected") + } + break LOOP2 + case <-time.After(2 * time.Second): + t.Fatal("no data in sink 2") + } + } + }) + } +} + func TestHubBroadcastJoin(t *testing.T) { tcs := []struct { name string @@ -621,8 +918,8 @@ func TestHubSubscriptions(t *testing.T) { c, err := newClient(context.Background(), defaultTestNode(), newTestTransport(func() {})) require.NoError(t, err) - _, _ = h.addSub("test1", c) - _, _ = h.addSub("test2", c) + _, _ = h.addSub("test1", subInfo{client: c, deltaType: ""}) + _, _ = h.addSub("test2", subInfo{client: c, deltaType: ""}) require.Equal(t, 2, h.NumChannels()) require.Contains(t, h.Channels(), "test1") require.Contains(t, h.Channels(), "test2") @@ -664,7 +961,7 @@ func TestUserConnections(t *testing.T) { _ = h.add(c) connections := h.UserConnections(c.UserID()) - require.Equal(t, h.connShards[index(c.UserID(), numHubShards)].conns, connections) + require.Equal(t, h.connShards[index(c.UserID(), numHubShards)].clients, connections) } func TestHubSharding(t *testing.T) { @@ -687,7 +984,7 @@ func TestHubSharding(t *testing.T) { require.NoError(t, err) _ = n.hub.add(c) for _, ch := range channels { - _, _ = n.hub.addSub(ch, c) + _, _ = n.hub.addSub(ch, subInfo{client: c, deltaType: ""}) } } } @@ -726,7 +1023,7 @@ func BenchmarkHub_Contention(b *testing.B) { _ = n.hub.add(c) clients = append(clients, c) for _, ch := range channels { - _, _ = n.hub.addSub(ch, c) + _, _ = n.hub.addSub(ch, subInfo{client: c, deltaType: ""}) } } @@ -746,7 +1043,7 @@ func BenchmarkHub_Contention(b *testing.B) { defer wg.Done() _ = n.hub.BroadcastPublication(channels[(i+numChannels/2)%numChannels], pub, streamPosition) }() - _, _ = n.hub.addSub(channels[i%numChannels], clients[i%numClients]) + _, _ = n.hub.addSub(channels[i%numChannels], subInfo{client: clients[i%numClients], deltaType: ""}) wg.Wait() } }) @@ -755,9 +1052,11 @@ func BenchmarkHub_Contention(b *testing.B) { var broadcastBenches = []struct { NumSubscribers int }{ + {1}, + {10}, + {100}, {1000}, {10000}, - {100000}, } // BenchmarkHub_MassiveBroadcast allows estimating time to broadcast @@ -768,27 +1067,20 @@ func BenchmarkHub_MassiveBroadcast(b *testing.B) { for _, tt := range broadcastBenches { numSubscribers := tt.NumSubscribers - b.Run(fmt.Sprintf("%d", numSubscribers), func(b *testing.B) { + b.Run(fmt.Sprintf("subscribers_%d", numSubscribers), func(b *testing.B) { b.ReportAllocs() n := defaultTestNodeBenchmark(b) - numChannels := 64 - channels := make([]string, 0, numChannels) - - for i := 0; i < numChannels; i++ { - channels = append(channels, "broadcast"+strconv.Itoa(i)) - } + channel := "broadcast" - sink := make(chan []byte, 1024) + sink := make(chan []byte, 10000) for i := 0; i < numSubscribers; i++ { t := newTestTransport(func() {}) t.setSink(sink) c := newTestConnectedClientWithTransport(b, context.Background(), n, t, "12") _ = n.hub.add(c) - for _, ch := range channels { - _, _ = n.hub.addSub(ch, c) - } + _, _ = n.hub.addSub(channel, subInfo{client: c, deltaType: ""}) } b.ResetTimer() @@ -806,7 +1098,7 @@ func BenchmarkHub_MassiveBroadcast(b *testing.B) { } } }() - _ = n.hub.BroadcastPublication(channels[i%numChannels], pub, streamPosition) + _ = n.hub.broadcastPublication(channel, streamPosition, pub, nil, nil) wg.Wait() } }) @@ -899,3 +1191,124 @@ func TestHubBroadcastInappropriateProtocol_Leave(t *testing.T) { testFunc(client) }) } + +var testJsonData = []byte(`{ + "_id":"662fb7df5110d6e8e9942fb2", + "index":0, + "guid":"a100afc6-fc35-47fd-8e3e-e8e9a81629ec", + "isActive":true, + "balance":"$2,784.25", + "picture":"http://placehold.it/32x32", + "age":21, + "eyeColor":"green", + "name":"Lois Norris", + "gender":"female", + "company":"ORGANICA", + "email":"loisnorris@organica.com", + "phone":"+1 (939) 451-2349", + "address":"774 Ide Court, Sabillasville, Virginia, 4034", + "about":"Cupidatat reprehenderit laboris aute pariatur nulla exercitation. Commodo aliqua cupidatat consectetur aliquip. Id irure nisi qui ullamco culpa reprehenderit nisi sunt consequat ipsum. Velit officia sint id voluptate anim. Sunt duis duis consequat mollit incididunt laborum enim amet ad aliqua esse nulla. Aliqua nulla adipisicing ad aliquip ut. Nostrud mollit ex aute magna culpa ea exercitation qui ex.\r\n", + "registered":"2023-02-28T11:09:34 -02:00", + "latitude":24.054483, + "longitude":38.953522, + "tags":[ + "consequat", + "adipisicing", + "eiusmod", + "ipsum", + "enim", + "et", + "voluptate" + ], + "friends":[ + { + "id":0, + "name":"Kaufman Randall" + }, + { + "id":1, + "name":"Byrd Cooley" + }, + { + "id":2, + "name":"Obrien William" + } + ], + "greeting":"Hello, Lois Norris! You have 9 unread messages.", + "favoriteFruit":"banana" +}`) + +// Has some changes (in tags field, in friends field). +var testNewJsonData = []byte(`{ + "_id":"662fb7df5110d6e8e9942fb2", + "index":0, + "guid":"a100afc6-fc35-47fd-8e3e-e8e9a81629ec", + "isActive":true, + "balance":"$2,784.25", + "picture":"http://placehold.it/32x32", + "age":21, + "eyeColor":"green", + "name":"Lois Norris", + "gender":"female", + "company":"ORGANICA", + "email":"loisnorris@organica.com", + "phone":"+1 (939) 451-2349", + "address":"774 Ide Court, Sabillasville, Virginia, 4034", + "about":"Cupidatat reprehenderit laboris aute pariatur nulla exercitation. Commodo aliqua cupidatat consectetur aliquip. Id irure nisi qui ullamco culpa reprehenderit nisi sunt consequat ipsum. Velit officia sint id voluptate anim. Sunt duis duis consequat mollit incididunt laborum enim amet ad aliqua esse nulla. Aliqua nulla adipisicing ad aliquip ut. Nostrud mollit ex aute magna culpa ea exercitation qui ex.\r\n", + "registered":"2023-02-28T11:09:34 -02:00", + "latitude":24.054483, + "longitude":38.953522, + "tags":[ + "consequat", + "adipisicing", + "eiusmod" + ], + "friends":[ + { + "id":0, + "name":"Kaufman Randall" + }, + { + "id":1, + "name":"Byrd Cooley" + } + ], + "greeting":"Hello, Lois Norris! You have 9 unread messages.", + "favoriteFruit":"banana" +}`) + +func TestJsonStringEncode(t *testing.T) { + testBenchmarkDeltaFossilPatch = fdelta.Create(testJsonData, testNewJsonData) + if len(testBenchmarkDeltaFossilPatch) == 0 { + t.Fatal("empty fossil patch") + } + testDeltaJsonData, err := json.Marshal(convert.BytesToString(testBenchmarkDeltaFossilPatch)) + require.NoError(t, err) + require.NotNil(t, testDeltaJsonData) + + alternativeDeltaJsonData := json.Escape(convert.BytesToString(testBenchmarkDeltaFossilPatch)) + require.Equal(t, testDeltaJsonData, alternativeDeltaJsonData) +} + +var testBenchmarkEncodeData []byte + +func BenchmarkEncodeJSONString(b *testing.B) { + jsonData := []byte(`{"input": "test"}`) + for i := 0; i < b.N; i++ { + testBenchmarkEncodeData = json.Escape(convert.BytesToString(jsonData)) + if len(testBenchmarkEncodeData) == 0 { + b.Fatal("empty data") + } + } +} + +var testBenchmarkDeltaFossilPatch []byte + +func BenchmarkDeltaFossil(b *testing.B) { + for i := 0; i < b.N; i++ { + testBenchmarkDeltaFossilPatch = fdelta.Create(testJsonData, testNewJsonData) + if len(testBenchmarkDeltaFossilPatch) == 0 { + b.Fatal("empty fossil patch") + } + } +} diff --git a/internal/redis_lua/broker_history_add_list.lua b/internal/redis_lua/broker_history_add_list.lua index 0e45b254..99bff5a1 100644 --- a/internal/redis_lua/broker_history_add_list.lua +++ b/internal/redis_lua/broker_history_add_list.lua @@ -9,6 +9,7 @@ local meta_expire = ARGV[5] local new_epoch_if_empty = ARGV[6] local publish_command = ARGV[7] local result_key_expire = ARGV[8] +local use_delta = ARGV[9] if result_key_expire ~= '' then local cached_result = redis.call("hmget", result_key, "e", "s") @@ -30,12 +31,20 @@ if meta_expire ~= '0' then redis.call("expire", meta_key, meta_expire) end +local prev_message_payload = "" +if use_delta == "1" then + prev_message_payload = redis.call("lindex", list_key, 0) or "" +end + local payload = "__" .. "p1:" .. top_offset .. ":" .. current_epoch .. "__" .. message_payload redis.call("lpush", list_key, payload) redis.call("ltrim", list_key, 0, ltrim_right_bound) redis.call("expire", list_key, list_ttl) if channel ~= '' then + if use_delta == "1" then + payload = "__" .. "d1:" .. top_offset .. ":" .. current_epoch .. ":" .. #prev_message_payload .. ":" .. prev_message_payload .. ":" .. #message_payload .. ":" .. message_payload + end redis.call(publish_command, channel, payload) end diff --git a/internal/redis_lua/broker_history_add_stream.lua b/internal/redis_lua/broker_history_add_stream.lua index 28251709..22065f33 100644 --- a/internal/redis_lua/broker_history_add_stream.lua +++ b/internal/redis_lua/broker_history_add_stream.lua @@ -9,6 +9,7 @@ local meta_expire = ARGV[5] local new_epoch_if_empty = ARGV[6] local publish_command = ARGV[7] local result_key_expire = ARGV[8] +local use_delta = ARGV[9] if result_key_expire ~= '' then local cached_result = redis.call("hmget", result_key, "e", "s") @@ -30,11 +31,34 @@ if meta_expire ~= '0' then redis.call("expire", meta_key, meta_expire) end +local prev_message_payload = "" +if use_delta == "1" then + local prev_entries = redis.call("xrevrange", stream_key, "+", "-", "COUNT", 1) + if #prev_entries > 0 then + prev_message_payload = prev_entries[1][2]["d"] + local fields_and_values = prev_entries[1][2] + -- Loop through the fields and values to find the field "d" + for i = 1, #fields_and_values, 2 do + local field = fields_and_values[i] + local value = fields_and_values[i + 1] + if field == "d" then + prev_message_payload = value + break -- Stop the loop once we find the field "d" + end + end + end +end + redis.call("xadd", stream_key, "MAXLEN", stream_size, top_offset, "d", message_payload) redis.call("expire", stream_key, stream_ttl) if channel ~= '' then - local payload = "__" .. "p1:" .. top_offset .. ":" .. current_epoch .. "__" .. message_payload + local payload + if use_delta == "1" then + payload = "__" .. "d1:" .. top_offset .. ":" .. current_epoch .. ":" .. #prev_message_payload .. ":" .. prev_message_payload .. ":" .. #message_payload .. ":" .. message_payload + else + payload = "__" .. "p1:" .. top_offset .. ":" .. current_epoch .. "__" .. message_payload + end redis.call(publish_command, channel, payload) end diff --git a/metrics.go b/metrics.go index 4359a953..ff15a1ff 100644 --- a/metrics.go +++ b/metrics.go @@ -47,20 +47,21 @@ type metrics struct { messagesSentCountLeave prometheus.Counter messagesSentCountControl prometheus.Counter - actionCountAddClient prometheus.Counter - actionCountRemoveClient prometheus.Counter - actionCountAddSub prometheus.Counter - actionCountRemoveSub prometheus.Counter - actionCountAddPresence prometheus.Counter - actionCountRemovePresence prometheus.Counter - actionCountPresence prometheus.Counter - actionCountPresenceStats prometheus.Counter - actionCountHistory prometheus.Counter - actionCountHistoryRecover prometheus.Counter - actionCountHistoryStreamTop prometheus.Counter - actionCountHistoryRemove prometheus.Counter - actionCountSurvey prometheus.Counter - actionCountNotify prometheus.Counter + actionCountAddClient prometheus.Counter + actionCountRemoveClient prometheus.Counter + actionCountAddSub prometheus.Counter + actionCountRemoveSub prometheus.Counter + actionCountAddPresence prometheus.Counter + actionCountRemovePresence prometheus.Counter + actionCountPresence prometheus.Counter + actionCountPresenceStats prometheus.Counter + actionCountHistory prometheus.Counter + actionCountHistoryRecover prometheus.Counter + actionCountHistoryStreamTop prometheus.Counter + actionCountHistoryStreamTopLatestPub prometheus.Counter + actionCountHistoryRemove prometheus.Counter + actionCountSurvey prometheus.Counter + actionCountNotify prometheus.Counter recoverCountYes prometheus.Counter recoverCountNo prometheus.Counter @@ -283,6 +284,8 @@ func (m *metrics) incActionCount(action string) { m.actionCountHistoryRecover.Inc() case "history_stream_top": m.actionCountHistoryStreamTop.Inc() + case "history_stream_top_latest_pub": + m.actionCountHistoryStreamTopLatestPub.Inc() case "history_remove": m.actionCountHistoryRemove.Inc() case "survey": @@ -465,6 +468,7 @@ func initMetricsRegistry(registry prometheus.Registerer, metricsNamespace string m.actionCountHistory = m.actionCount.WithLabelValues("history") m.actionCountHistoryRecover = m.actionCount.WithLabelValues("history_recover") m.actionCountHistoryStreamTop = m.actionCount.WithLabelValues("history_stream_top") + m.actionCountHistoryStreamTopLatestPub = m.actionCount.WithLabelValues("history_stream_top_latest_pub") m.actionCountHistoryRemove = m.actionCount.WithLabelValues("history_remove") m.actionCountSurvey = m.actionCount.WithLabelValues("survey") m.actionCountNotify = m.actionCount.WithLabelValues("notify") diff --git a/node.go b/node.go index 3cc346de..9df9dfb0 100644 --- a/node.go +++ b/node.go @@ -83,6 +83,8 @@ type Node struct { nodeInfoSendHandler NodeInfoSendHandler emulationSurveyHandler *emulationSurveyHandler + + mediums map[string]*channelMedium } const ( @@ -162,6 +164,7 @@ func New(c Config) (*Node, error) { subDissolver: dissolve.New(numSubDissolverWorkers), nowTimeGetter: nowtime.Get, surveyRegistry: make(map[uint64]chan survey), + mediums: map[string]*channelMedium{}, } n.emulationSurveyHandler = newEmulationSurveyHandler(n) @@ -683,14 +686,14 @@ func (n *Node) handleControl(data []byte) error { // handlePublication handles messages published into channel and // coming from Broker. The goal of method is to deliver this message // to all clients on this node currently subscribed to channel. -func (n *Node) handlePublication(ch string, pub *Publication, sp StreamPosition) error { +func (n *Node) handlePublication(ch string, sp StreamPosition, pub, prevPub, localPrevPub *Publication) error { n.metrics.incMessagesReceived("publication") numSubscribers := n.hub.NumSubscribers(ch) hasCurrentSubscribers := numSubscribers > 0 if !hasCurrentSubscribers { return nil } - return n.hub.BroadcastPublication(ch, pub, sp) + return n.hub.broadcastPublication(ch, sp, pub, prevPub, localPrevPub) } // handleJoin handles join messages - i.e. broadcasts it to @@ -971,19 +974,37 @@ func (n *Node) removeClient(c *Client) error { // addSubscription registers subscription of connection on channel in both // Hub and Broker. -func (n *Node) addSubscription(ch string, c *Client) error { +func (n *Node) addSubscription(ch string, sub subInfo) error { n.metrics.incActionCount("add_subscription") mu := n.subLock(ch) mu.Lock() defer mu.Unlock() - first, err := n.hub.addSub(ch, c) + first, err := n.hub.addSub(ch, sub) if err != nil { return err } if first { + if n.config.GetChannelMediumOptions != nil { + mediumOptions, ok := n.config.GetChannelMediumOptions(ch) + if ok { + medium, err := newChannelMedium(ch, n, mediumOptions) + if err != nil { + return err + } + n.mediums[ch] = medium + } + } + err := n.broker.Subscribe(ch) if err != nil { - _, _ = n.hub.removeSub(ch, c) + _, _ = n.hub.removeSub(ch, sub.client) + if n.config.GetChannelMediumOptions != nil { + medium, ok := n.mediums[ch] + if ok { + medium.close() + delete(n.mediums, ch) + } + } return err } } @@ -1017,6 +1038,12 @@ func (n *Node) removeSubscription(ch string, c *Client) error { if err != nil { // Cool down a bit since broker is not ready to process unsubscription. time.Sleep(500 * time.Millisecond) + } else { + medium, ok := n.mediums[ch] + if ok { + medium.close() + delete(n.mediums, ch) + } } return err } @@ -1337,6 +1364,29 @@ func (n *Node) recoverHistory(ch string, since StreamPosition, historyMetaTTL ti }), WithHistoryMetaTTL(historyMetaTTL)) } +// recoverCache recovers last publication in channel. +func (n *Node) recoverCache(ch string, historyMetaTTL time.Duration) (*Publication, StreamPosition, error) { + n.metrics.incActionCount("history_recover") + return n.streamTopLatestPub(ch, historyMetaTTL) +} + +// streamTopLatestPub returns latest publication in channel with actual stream position. +func (n *Node) streamTopLatestPub(ch string, historyMetaTTL time.Duration) (*Publication, StreamPosition, error) { + n.metrics.incActionCount("history_stream_top_latest_pub") + hr, err := n.History(ch, WithHistoryFilter(HistoryFilter{ + Limit: 1, + Reverse: true, + }), WithHistoryMetaTTL(historyMetaTTL)) + if err != nil { + return nil, StreamPosition{}, err + } + var latestPublication *Publication + if len(hr.Publications) > 0 { + latestPublication = hr.Publications[0] + } + return latestPublication, hr.StreamPosition, nil +} + // streamTop returns current stream top StreamPosition for a channel. func (n *Node) streamTop(ch string, historyMetaTTL time.Duration) (StreamPosition, error) { n.metrics.incActionCount("history_stream_top") @@ -1347,6 +1397,24 @@ func (n *Node) streamTop(ch string, historyMetaTTL time.Duration) (StreamPositio return historyResult.StreamPosition, nil } +func (n *Node) checkPosition(ch string, clientPosition StreamPosition, historyMetaTTL time.Duration) (bool, error) { + mu := n.subLock(ch) + mu.Lock() + medium, ok := n.mediums[ch] + mu.Unlock() + if !ok || !medium.options.EnablePositionSync { + // No medium for channel or position sync disabled – we then check position over Broker. + streamTop, err := n.streamTop(ch, historyMetaTTL) + if err != nil { + // Will be checked later. + return false, err + } + return streamTop.Epoch == clientPosition.Epoch && clientPosition.Offset == streamTop.Offset, nil + } + validPosition := medium.CheckPosition(historyMetaTTL, clientPosition, n.config.ClientChannelPositionCheckDelay) + return validPosition, nil +} + // RemoveHistory removes channel history. func (n *Node) RemoveHistory(ch string) error { n.metrics.incActionCount("history_remove") @@ -1480,6 +1548,7 @@ type eventHub struct { transportWriteHandler TransportWriteHandler commandReadHandler CommandReadHandler commandProcessedHandler CommandProcessedHandler + cacheEmptyHandler CacheEmptyHandler } // OnConnecting allows setting ConnectingHandler. @@ -1512,16 +1581,34 @@ func (n *Node) OnCommandProcessed(handler CommandProcessedHandler) { n.clientEvents.commandProcessedHandler = handler } +// OnCacheEmpty allows setting CacheEmptyHandler. +// CacheEmptyHandler called when client subscribes on a channel with RecoveryModeCache but there is no +// cached value in channel. In response to this handler it's possible to tell Centrifuge what to do with +// subscribe request – keep it, or return error. +func (n *Node) OnCacheEmpty(h CacheEmptyHandler) { + n.clientEvents.cacheEmptyHandler = h +} + type brokerEventHandler struct { node *Node } // HandlePublication coming from Broker. -func (h *brokerEventHandler) HandlePublication(ch string, pub *Publication, sp StreamPosition) error { +func (h *brokerEventHandler) HandlePublication(ch string, pub *Publication, sp StreamPosition, prevPub *Publication) error { if pub == nil { panic("nil Publication received, this must never happen") } - return h.node.handlePublication(ch, pub, sp) + if h.node.config.GetChannelMediumOptions != nil { + mu := h.node.subLock(ch) + mu.Lock() + medium, ok := h.node.mediums[ch] + mu.Unlock() + if ok { + medium.broadcastPublication(pub, sp, prevPub) + return nil + } + } + return h.node.handlePublication(ch, sp, pub, prevPub, nil) } // HandleJoin coming from Broker. diff --git a/node_test.go b/node_test.go index 16805e89..a31b7edc 100644 --- a/node_test.go +++ b/node_test.go @@ -32,6 +32,8 @@ type TestBroker struct { publishJoinCount int32 publishLeaveCount int32 publishControlCount int32 + + historyFunc func(_ string, _ HistoryOptions) ([]*Publication, StreamPosition, error) } func NewTestBroker() *TestBroker { @@ -91,7 +93,10 @@ func (e *TestBroker) Unsubscribe(_ string) error { return nil } -func (e *TestBroker) History(_ string, _ HistoryOptions) ([]*Publication, StreamPosition, error) { +func (e *TestBroker) History(ch string, opts HistoryOptions) ([]*Publication, StreamPosition, error) { + if e.historyFunc != nil { + return e.historyFunc(ch, opts) + } if e.errorOnHistory { return nil, StreamPosition{}, errors.New("boom") } @@ -1170,7 +1175,7 @@ func TestBrokerEventHandler_PanicsOnNil(t *testing.T) { defer func() { _ = node.Shutdown(context.Background()) }() handler := &brokerEventHandler{node: node} require.Panics(t, func() { - _ = handler.HandlePublication("test", nil, StreamPosition{}) + _ = handler.HandlePublication("test", nil, StreamPosition{}, nil) }) require.Panics(t, func() { _ = handler.HandleJoin("test", nil) @@ -1344,3 +1349,37 @@ func TestNode_OnCommandRead(t *testing.T) { require.Fail(t, "timeout subscribe") } } + +func TestNodeCheckPosition(t *testing.T) { + node := defaultTestNode() + defer func() { _ = node.Shutdown(context.Background()) }() + + broker := NewTestBroker() + broker.historyFunc = func(channel string, opts HistoryOptions) ([]*Publication, StreamPosition, error) { + return nil, StreamPosition{ + Offset: 20, Epoch: "test", + }, nil + } + node.SetBroker(broker) + + isValid, err := node.checkPosition("test", StreamPosition{ + Offset: 20, + Epoch: "test", + }, 200*time.Second) + require.NoError(t, err) + require.True(t, isValid) + + isValid, err = node.checkPosition("test", StreamPosition{ + Offset: 19, + Epoch: "test", + }, 200*time.Second) + require.NoError(t, err) + require.False(t, isValid) + + isValid, err = node.checkPosition("test", StreamPosition{ + Offset: 20, + Epoch: "test_new", + }, 200*time.Second) + require.NoError(t, err) + require.False(t, isValid) +} diff --git a/options.go b/options.go index 1ed740f4..f995d00e 100644 --- a/options.go +++ b/options.go @@ -24,6 +24,13 @@ func WithIdempotencyKey(key string) PublishOption { } } +// WithDelta tells Broker to use delta streaming. +func WithDelta(enabled bool) PublishOption { + return func(opts *PublishOptions) { + opts.UseDelta = enabled + } +} + // WithIdempotentResultTTL sets the time of expiration for results of idempotent publications. // See PublishOptions.IdempotentResultTTL for more description and defaults. func WithIdempotentResultTTL(ttl time.Duration) PublishOption { @@ -78,6 +85,8 @@ type SubscribeOptions struct { // Make sure you are using EnableRecovery in channels that maintain Publication // history stream. EnableRecovery bool + // RecoveryMode is by default RecoveryModeStream, but can be also RecoveryModeCache. + RecoveryMode RecoveryMode // Data to send to a client with Subscribe Push. Data []byte // RecoverSince will try to subscribe a client and recover from a certain StreamPosition. @@ -87,6 +96,12 @@ type SubscribeOptions struct { // meta information expiration time. HistoryMetaTTL time.Duration + // AllowedDeltaTypes is a whitelist of DeltaType subscribers can negotiate. At this point Centrifuge + // only supports DeltaTypeFossil. If zero value – clients won't be able to negotiate delta encoding + // within a channel and will receive full data in publications. + // Delta encoding is an EXPERIMENTAL feature and may be changed. + AllowedDeltaTypes []DeltaType + // clientID to subscribe. clientID string // sessionID to subscribe. @@ -148,6 +163,20 @@ func WithRecovery(enabled bool) SubscribeOption { } } +type RecoveryMode int32 + +const ( + RecoveryModeStream RecoveryMode = 0 + RecoveryModeCache RecoveryMode = 1 +) + +// WithRecoveryMode ... +func WithRecoveryMode(mode RecoveryMode) SubscribeOption { + return func(opts *SubscribeOptions) { + opts.RecoveryMode = mode + } +} + // WithSubscribeClient allows setting client ID that should be subscribed. // This option not used when Client.Subscribe called. func WithSubscribeClient(clientID string) SubscribeOption { diff --git a/options_test.go b/options_test.go index 7a9ecb80..1a1adb1b 100644 --- a/options_test.go +++ b/options_test.go @@ -22,6 +22,13 @@ func TestWithIdempotencyKey(t *testing.T) { require.Equal(t, "ik", opts.IdempotencyKey) } +func TestWithDelta(t *testing.T) { + opt := WithDelta(true) + opts := &PublishOptions{} + opt(opts) + require.True(t, opts.UseDelta) +} + func TestWithIdempotentResultTTL(t *testing.T) { opt := WithIdempotentResultTTL(time.Minute) opts := &PublishOptions{} @@ -48,6 +55,7 @@ func TestSubscribeOptions(t *testing.T) { WithSubscribeSession("session"), WithSubscribeClient("test"), WithSubscribeSource(4), + WithRecoveryMode(RecoveryModeCache), } opts := &SubscribeOptions{} for _, opt := range subscribeOpts { @@ -63,6 +71,7 @@ func TestSubscribeOptions(t *testing.T) { require.Equal(t, "test", opts.clientID) require.Equal(t, "session", opts.sessionID) require.Equal(t, uint8(4), opts.Source) + require.Equal(t, RecoveryModeCache, opts.RecoveryMode) } func TestWithDisconnect(t *testing.T) {