<commit_before>// +build js

package main

import (
	"html"
	"strings"

	"github.com/gopherjs/gopherjs/js"

	"honnef.co/go/js/dom"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

var headers []dom.Element

var selected int

var baseHash string
var baseX, baseY int

var entryHeight float64
var entries []dom.Node
var manuallyPicked string

func main() {}

func init() {
	document.AddEventListener("DOMContentLoaded", false, func(_ dom.Event) {
		setup()
	})
}

func setup() {
	overlay := document.CreateElement("div").(*dom.HTMLDivElement)
	overlay.SetID("gts-overlay")

	container := document.CreateElement("div")
	overlay.AppendChild(container)
	container.Underlying().Set("outerHTML", `<div><input id="gts-command"></input><div id="gts-results"></div></div>`)

	document.Body().AppendChild(overlay)

	command := document.GetElementByID("gts-command").(*dom.HTMLInputElement)
	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	command.AddEventListener("input", false, func(event dom.Event) {
		updateResults(false, nil)
	})

	/*mousedown := false
	results.AddEventListener("mousedown", false, func(event dom.Event) {
		mousedown = true

		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})
	results.AddEventListener("mouseup", false, func(event dom.Event) {
		mousedown = false
	})
	results.AddEventListener("mouseleave", false, func(event dom.Event) {
		mousedown = false
	})
	results.AddEventListener("mousemove", false, func(event dom.Event) {
		if !mousedown {
			return
		}

		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})*/
	results.AddEventListener("click", false, func(event dom.Event) {
		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})
	results.AddEventListener("dblclick", false, func(event dom.Event) {
		event.PreventDefault()

		hideOverlay(overlay)
	})

	overlay.AddEventListener("keydown", false, func(event dom.Event) {
		switch ke := event.(*dom.KeyboardEvent); {
		case ke.KeyCode == 27 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Escape.
			ke.PreventDefault()

			if ke.Target().Underlying() == command.Underlying() {
				js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+baseHash)
				dom.GetWindow().ScrollTo(baseX, baseY)
			}

			hideOverlay(overlay)
		case ke.KeyCode == 13 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Enter.
			ke.PreventDefault()

			hideOverlay(overlay)
		case ke.KeyCode == 40 && !ke.CtrlKey && !ke.AltKey && ke.MetaKey && !ke.ShiftKey: // Down.
			ke.PreventDefault()
			selected = len(entries) - 1
			updateResultSelection()
		case ke.KeyCode == 40 && ke.CtrlKey && ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Down.
			ke.PreventDefault()
			results.Underlying().Set("scrollTop", results.Underlying().Get("scrollTop").Float()+entryHeight)
		case ke.KeyCode == 40 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Down.
			ke.PreventDefault()
			selected++
			updateResultSelection()
		case ke.KeyCode == 38 && !ke.CtrlKey && !ke.AltKey && ke.MetaKey && !ke.ShiftKey: // Up.
			ke.PreventDefault()
			selected = 0
			updateResultSelection()
		case ke.KeyCode == 38 && ke.CtrlKey && ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Up.
			ke.PreventDefault()
			results.Underlying().Set("scrollTop", results.Underlying().Get("scrollTop").Float()-entryHeight)
		case ke.KeyCode == 38 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Up.
			ke.PreventDefault()
			selected--
			updateResultSelection()
		}
	})

	document.Body().AddEventListener("keydown", false, func(event dom.Event) {
		switch ke := event.(*dom.KeyboardEvent); {
		case ke.KeyCode == int('F') && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // F.
			fallthrough
		case ke.KeyCode == int('R') && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Just R, since some browsers don't let us intercept Cmd+R.
			// Ignore just R when command elment has focus (it means the user is typing).
			if ke.Target().Underlying() == command.Underlying() {
				break
			}
			fallthrough
		case ke.KeyCode == int('R') && !ke.CtrlKey && !ke.AltKey && ke.MetaKey && !ke.ShiftKey: // Cmd+R.
			ke.PreventDefault()

			// Is overlay already being displayed?
			if isOverlayVisible(overlay) {
				command.Select()
				break
			}

			command.Value = ""
			manuallyPicked = ""

			{
				headers = nil
				for _, header := range append(document.Body().GetElementsByTagName("h3"), document.Body().GetElementsByTagName("h4")...) {
					if header.ID() == "" {
						continue
					}
					headers = append(headers, header)
				}

				baseHash = strings.TrimPrefix(dom.GetWindow().Location().Hash, "#")
				baseX, baseY = dom.GetWindow().ScrollX(), dom.GetWindow().ScrollY()

				updateResults(true, overlay)
			}

			command.Select()
		case ke.KeyCode == 27 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Escape.
			// If overlay is not displayed, there's nothing to do.
			if !isOverlayVisible(overlay) {
				break
			}

			hideOverlay(overlay)

			ke.PreventDefault()
		}
	})
}

var previouslyHighlightedHeader dom.HTMLElement

func hideOverlay(overlay dom.HTMLElement) {
	overlay.Style().SetProperty("display", "none", "")

	if previouslyHighlightedHeader != nil {
		previouslyHighlightedHeader.Class().Remove("highlighted")
		previouslyHighlightedHeader.Class().Add("highlighted-fade")
	}

	document.GetElementByID("gts-command").(dom.HTMLElement).Blur() // Deselect the command input; needed in Firefox so body regains focus.
}

// isOverlayVisible returns true iff overlay is being displayed.
func isOverlayVisible(overlay dom.HTMLElement) bool {
	return (overlay.Style().GetPropertyValue("display") == "initial")
}

var previouslySelected int

func updateResultSelection() {
	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	if selected < 0 {
		selected = 0
	} else if selected > len(entries)-1 {
		selected = len(entries) - 1
	}

	if selected == previouslySelected {
		return
	}

	entries[previouslySelected].(dom.Element).Class().Remove("gts-highlighted")
	if previouslyHighlightedHeader != nil {
		previouslyHighlightedHeader.Class().Remove("highlighted")
	}

	{
		element := entries[selected].(dom.Element)

		if element.GetBoundingClientRect().Top < results.GetBoundingClientRect().Top {
			element.Underlying().Call("scrollIntoView", true)
		} else if element.GetBoundingClientRect().Bottom > results.GetBoundingClientRect().Bottom {
			element.Underlying().Call("scrollIntoView", false)
		}

		element.Class().Add("gts-highlighted")
		//dom.GetWindow().Location().Hash = "#" + element.GetAttribute("data-id")
		//dom.GetWindow().History().ReplaceState(nil, nil, "#"+element.GetAttribute("data-id"))
		js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+element.GetAttribute("data-id"))
		target := document.GetElementByID(element.GetAttribute("data-id")).(dom.HTMLElement)
		target.Class().Add("highlighted")
		previouslyHighlightedHeader = target
		centerOnTargetIfOffscreen(target)

		manuallyPicked = element.GetAttribute("data-id")
	}

	previouslySelected = selected
}

// offsetTopRoot returns the offset top of element e relative to root element.
func offsetTopRoot(e dom.HTMLElement) float64 {
	var offsetTopRoot float64
	for ; e != nil; e = e.OffsetParent() {
		offsetTopRoot += e.OffsetTop()
	}
	return offsetTopRoot
}

func centerOnTargetIfOffscreen(target dom.HTMLElement) {
	isOffscreen := int(offsetTopRoot(target)) < dom.GetWindow().ScrollY() ||
		int(offsetTopRoot(target)+target.OffsetHeight()) > dom.GetWindow().ScrollY()+dom.GetWindow().InnerHeight()

	if isOffscreen {
		windowHalfHeight := dom.GetWindow().InnerHeight() / 2

		dom.GetWindow().ScrollTo(dom.GetWindow().ScrollX(), int(offsetTopRoot(target)+target.OffsetHeight())-windowHalfHeight)
	}
}

var initialSelected int

func updateResults(init bool, overlay dom.HTMLElement) {
	windowHalfHeight := dom.GetWindow().InnerHeight() / 2
	filter := document.GetElementByID("gts-command").(*dom.HTMLInputElement).Value

	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	var selectionPreserved = false

	results.SetInnerHTML("")
	var visibleIndex int
	for _, header := range headers {
		if filter != "" && !strings.Contains(strings.ToLower(header.TextContent()), strings.ToLower(filter)) {
			continue
		}

		element := document.CreateElement("div")
		element.Class().Add("gts-entry")
		element.SetAttribute("data-id", header.ID())
		{
			entry := header.TextContent()
			index := strings.Index(strings.ToLower(entry), strings.ToLower(filter))
			element.SetInnerHTML(html.EscapeString(entry[:index]) + "<strong>" + html.EscapeString(entry[index:index+len(filter)]) + "</strong>" + html.EscapeString(entry[index+len(filter):]))
		}
		if header.ID() == manuallyPicked {
			selectionPreserved = true

			selected = visibleIndex
			previouslySelected = visibleIndex
		}

		results.AppendChild(element)

		visibleIndex++
	}

	entries = results.ChildNodes()

	if !selectionPreserved {
		manuallyPicked = ""

		if init {
			// Find the nearest entry.
			for i := len(entries) - 1; i >= 0; i-- {
				element := entries[i].(dom.Element)
				header := document.GetElementByID(element.GetAttribute("data-id"))

				if header.GetBoundingClientRect().Top <= windowHalfHeight || i == 0 {
					selected = i
					previouslySelected = i

					initialSelected = i

					break
				}
			}
		} else {
			if filter == "" {
				selected = initialSelected
				previouslySelected = initialSelected
			} else {
				selected = 0
				previouslySelected = 0
			}
		}
	}

	if init {
		if previouslyHighlightedHeader != nil {
			previouslyHighlightedHeader.Class().Remove("highlighted-fade")
		}

		overlay.Style().SetProperty("display", "initial", "")
		entryHeight = results.FirstChild().(dom.Element).GetBoundingClientRect().Object.Get("height").Float()
	} else {
		if previouslyHighlightedHeader != nil {
			previouslyHighlightedHeader.Class().Remove("highlighted")
		}
	}

	if len(entries) > 0 {
		element := entries[selected].(dom.Element)

		if init {
			y := float64(selected) * entryHeight
			results.Underlying().Set("scrollTop", y-float64(results.GetBoundingClientRect().Height/2))
		} else {
			if element.GetBoundingClientRect().Top <= results.GetBoundingClientRect().Top {
				element.Underlying().Call("scrollIntoView", true)
			} else if element.GetBoundingClientRect().Bottom >= results.GetBoundingClientRect().Bottom {
				element.Underlying().Call("scrollIntoView", false)
			}
		}

		element.Class().Add("gts-highlighted")
		//dom.GetWindow().Location().Hash = "#" + element.GetAttribute("data-id")
		//dom.GetWindow().History().ReplaceState(nil, nil, "#"+element.GetAttribute("data-id"))
		js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+element.GetAttribute("data-id"))
		target := document.GetElementByID(element.GetAttribute("data-id")).(dom.HTMLElement)
		target.Class().Add("highlighted")
		previouslyHighlightedHeader = target
		centerOnTargetIfOffscreen(target)
	}
}
<commit_msg>Use IsEqualNode to compare nodes of .Underlying() ==.<commit_after>// +build js

package main

import (
	"html"
	"strings"

	"github.com/gopherjs/gopherjs/js"

	"honnef.co/go/js/dom"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

var headers []dom.Element

var selected int

var baseHash string
var baseX, baseY int

var entryHeight float64
var entries []dom.Node
var manuallyPicked string

func main() {}

func init() {
	document.AddEventListener("DOMContentLoaded", false, func(_ dom.Event) {
		setup()
	})
}

func setup() {
	overlay := document.CreateElement("div").(*dom.HTMLDivElement)
	overlay.SetID("gts-overlay")

	container := document.CreateElement("div")
	overlay.AppendChild(container)
	container.Underlying().Set("outerHTML", `<div><input id="gts-command"></input><div id="gts-results"></div></div>`)

	document.Body().AppendChild(overlay)

	command := document.GetElementByID("gts-command").(*dom.HTMLInputElement)
	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	command.AddEventListener("input", false, func(event dom.Event) {
		updateResults(false, nil)
	})

	/*mousedown := false
	results.AddEventListener("mousedown", false, func(event dom.Event) {
		mousedown = true

		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})
	results.AddEventListener("mouseup", false, func(event dom.Event) {
		mousedown = false
	})
	results.AddEventListener("mouseleave", false, func(event dom.Event) {
		mousedown = false
	})
	results.AddEventListener("mousemove", false, func(event dom.Event) {
		if !mousedown {
			return
		}

		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})*/
	results.AddEventListener("click", false, func(event dom.Event) {
		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})
	results.AddEventListener("dblclick", false, func(event dom.Event) {
		event.PreventDefault()

		hideOverlay(overlay)
	})

	overlay.AddEventListener("keydown", false, func(event dom.Event) {
		switch ke := event.(*dom.KeyboardEvent); {
		case ke.KeyCode == 27 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Escape.
			ke.PreventDefault()

			if ke.Target().IsEqualNode(command) {
				js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+baseHash)
				dom.GetWindow().ScrollTo(baseX, baseY)
			}

			hideOverlay(overlay)
		case ke.KeyCode == 13 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Enter.
			ke.PreventDefault()

			hideOverlay(overlay)
		case ke.KeyCode == 40 && !ke.CtrlKey && !ke.AltKey && ke.MetaKey && !ke.ShiftKey: // Down.
			ke.PreventDefault()
			selected = len(entries) - 1
			updateResultSelection()
		case ke.KeyCode == 40 && ke.CtrlKey && ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Down.
			ke.PreventDefault()
			results.Underlying().Set("scrollTop", results.Underlying().Get("scrollTop").Float()+entryHeight)
		case ke.KeyCode == 40 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Down.
			ke.PreventDefault()
			selected++
			updateResultSelection()
		case ke.KeyCode == 38 && !ke.CtrlKey && !ke.AltKey && ke.MetaKey && !ke.ShiftKey: // Up.
			ke.PreventDefault()
			selected = 0
			updateResultSelection()
		case ke.KeyCode == 38 && ke.CtrlKey && ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Up.
			ke.PreventDefault()
			results.Underlying().Set("scrollTop", results.Underlying().Get("scrollTop").Float()-entryHeight)
		case ke.KeyCode == 38 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Up.
			ke.PreventDefault()
			selected--
			updateResultSelection()
		}
	})

	document.Body().AddEventListener("keydown", false, func(event dom.Event) {
		switch ke := event.(*dom.KeyboardEvent); {
		case ke.KeyCode == int('F') && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // F.
			fallthrough
		case ke.KeyCode == int('R') && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Just R, since some browsers don't let us intercept Cmd+R.
			// Ignore just R when command elment has focus (it means the user is typing).
			if ke.Target().IsEqualNode(command) {
				break
			}
			fallthrough
		case ke.KeyCode == int('R') && !ke.CtrlKey && !ke.AltKey && ke.MetaKey && !ke.ShiftKey: // Cmd+R.
			ke.PreventDefault()

			// Is overlay already being displayed?
			if isOverlayVisible(overlay) {
				command.Select()
				break
			}

			command.Value = ""
			manuallyPicked = ""

			{
				headers = nil
				for _, header := range append(document.Body().GetElementsByTagName("h3"), document.Body().GetElementsByTagName("h4")...) {
					if header.ID() == "" {
						continue
					}
					headers = append(headers, header)
				}

				baseHash = strings.TrimPrefix(dom.GetWindow().Location().Hash, "#")
				baseX, baseY = dom.GetWindow().ScrollX(), dom.GetWindow().ScrollY()

				updateResults(true, overlay)
			}

			command.Select()
		case ke.KeyCode == 27 && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey: // Escape.
			// If overlay is not displayed, there's nothing to do.
			if !isOverlayVisible(overlay) {
				break
			}

			hideOverlay(overlay)

			ke.PreventDefault()
		}
	})
}

var previouslyHighlightedHeader dom.HTMLElement

func hideOverlay(overlay dom.HTMLElement) {
	overlay.Style().SetProperty("display", "none", "")

	if previouslyHighlightedHeader != nil {
		previouslyHighlightedHeader.Class().Remove("highlighted")
		previouslyHighlightedHeader.Class().Add("highlighted-fade")
	}

	document.GetElementByID("gts-command").(dom.HTMLElement).Blur() // Deselect the command input; needed in Firefox so body regains focus.
}

// isOverlayVisible returns true iff overlay is being displayed.
func isOverlayVisible(overlay dom.HTMLElement) bool {
	return (overlay.Style().GetPropertyValue("display") == "initial")
}

var previouslySelected int

func updateResultSelection() {
	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	if selected < 0 {
		selected = 0
	} else if selected > len(entries)-1 {
		selected = len(entries) - 1
	}

	if selected == previouslySelected {
		return
	}

	entries[previouslySelected].(dom.Element).Class().Remove("gts-highlighted")
	if previouslyHighlightedHeader != nil {
		previouslyHighlightedHeader.Class().Remove("highlighted")
	}

	{
		element := entries[selected].(dom.Element)

		if element.GetBoundingClientRect().Top < results.GetBoundingClientRect().Top {
			element.Underlying().Call("scrollIntoView", true)
		} else if element.GetBoundingClientRect().Bottom > results.GetBoundingClientRect().Bottom {
			element.Underlying().Call("scrollIntoView", false)
		}

		element.Class().Add("gts-highlighted")
		//dom.GetWindow().Location().Hash = "#" + element.GetAttribute("data-id")
		//dom.GetWindow().History().ReplaceState(nil, nil, "#"+element.GetAttribute("data-id"))
		js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+element.GetAttribute("data-id"))
		target := document.GetElementByID(element.GetAttribute("data-id")).(dom.HTMLElement)
		target.Class().Add("highlighted")
		previouslyHighlightedHeader = target
		centerOnTargetIfOffscreen(target)

		manuallyPicked = element.GetAttribute("data-id")
	}

	previouslySelected = selected
}

// offsetTopRoot returns the offset top of element e relative to root element.
func offsetTopRoot(e dom.HTMLElement) float64 {
	var offsetTopRoot float64
	for ; e != nil; e = e.OffsetParent() {
		offsetTopRoot += e.OffsetTop()
	}
	return offsetTopRoot
}

func centerOnTargetIfOffscreen(target dom.HTMLElement) {
	isOffscreen := int(offsetTopRoot(target)) < dom.GetWindow().ScrollY() ||
		int(offsetTopRoot(target)+target.OffsetHeight()) > dom.GetWindow().ScrollY()+dom.GetWindow().InnerHeight()

	if isOffscreen {
		windowHalfHeight := dom.GetWindow().InnerHeight() / 2

		dom.GetWindow().ScrollTo(dom.GetWindow().ScrollX(), int(offsetTopRoot(target)+target.OffsetHeight())-windowHalfHeight)
	}
}

var initialSelected int

func updateResults(init bool, overlay dom.HTMLElement) {
	windowHalfHeight := dom.GetWindow().InnerHeight() / 2
	filter := document.GetElementByID("gts-command").(*dom.HTMLInputElement).Value

	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	var selectionPreserved = false

	results.SetInnerHTML("")
	var visibleIndex int
	for _, header := range headers {
		if filter != "" && !strings.Contains(strings.ToLower(header.TextContent()), strings.ToLower(filter)) {
			continue
		}

		element := document.CreateElement("div")
		element.Class().Add("gts-entry")
		element.SetAttribute("data-id", header.ID())
		{
			entry := header.TextContent()
			index := strings.Index(strings.ToLower(entry), strings.ToLower(filter))
			element.SetInnerHTML(html.EscapeString(entry[:index]) + "<strong>" + html.EscapeString(entry[index:index+len(filter)]) + "</strong>" + html.EscapeString(entry[index+len(filter):]))
		}
		if header.ID() == manuallyPicked {
			selectionPreserved = true

			selected = visibleIndex
			previouslySelected = visibleIndex
		}

		results.AppendChild(element)

		visibleIndex++
	}

	entries = results.ChildNodes()

	if !selectionPreserved {
		manuallyPicked = ""

		if init {
			// Find the nearest entry.
			for i := len(entries) - 1; i >= 0; i-- {
				element := entries[i].(dom.Element)
				header := document.GetElementByID(element.GetAttribute("data-id"))

				if header.GetBoundingClientRect().Top <= windowHalfHeight || i == 0 {
					selected = i
					previouslySelected = i

					initialSelected = i

					break
				}
			}
		} else {
			if filter == "" {
				selected = initialSelected
				previouslySelected = initialSelected
			} else {
				selected = 0
				previouslySelected = 0
			}
		}
	}

	if init {
		if previouslyHighlightedHeader != nil {
			previouslyHighlightedHeader.Class().Remove("highlighted-fade")
		}

		overlay.Style().SetProperty("display", "initial", "")
		entryHeight = results.FirstChild().(dom.Element).GetBoundingClientRect().Object.Get("height").Float()
	} else {
		if previouslyHighlightedHeader != nil {
			previouslyHighlightedHeader.Class().Remove("highlighted")
		}
	}

	if len(entries) > 0 {
		element := entries[selected].(dom.Element)

		if init {
			y := float64(selected) * entryHeight
			results.Underlying().Set("scrollTop", y-float64(results.GetBoundingClientRect().Height/2))
		} else {
			if element.GetBoundingClientRect().Top <= results.GetBoundingClientRect().Top {
				element.Underlying().Call("scrollIntoView", true)
			} else if element.GetBoundingClientRect().Bottom >= results.GetBoundingClientRect().Bottom {
				element.Underlying().Call("scrollIntoView", false)
			}
		}

		element.Class().Add("gts-highlighted")
		//dom.GetWindow().Location().Hash = "#" + element.GetAttribute("data-id")
		//dom.GetWindow().History().ReplaceState(nil, nil, "#"+element.GetAttribute("data-id"))
		js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+element.GetAttribute("data-id"))
		target := document.GetElementByID(element.GetAttribute("data-id")).(dom.HTMLElement)
		target.Class().Add("highlighted")
		previouslyHighlightedHeader = target
		centerOnTargetIfOffscreen(target)
	}
}
<|endoftext|>


<commit_before>// +build js

package main

import (
	"html"
	"strings"

	"github.com/gopherjs/gopherjs/js"

	"honnef.co/go/js/dom"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

var headers []dom.Element

var selected int

var baseHash string
var baseX, baseY int

var entryHeight float64
var entries []dom.Node
var manuallyPicked string

func main() {
	overlay := document.CreateElement("div").(*dom.HTMLDivElement)
	overlay.SetID("gts-overlay")

	container := document.CreateElement("div")
	overlay.AppendChild(container)
	container.Underlying().Set("outerHTML", `<div><input id="gts-command"></input><div id="gts-results"></div></div>`)

	document.Body().AppendChild(overlay)

	command := document.GetElementByID("gts-command").(*dom.HTMLInputElement)
	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	command.AddEventListener("input", false, func(event dom.Event) {
		updateResults(false, nil)
	})

	/*mousedown := false
	results.AddEventListener("mousedown", false, func(event dom.Event) {
		mousedown = true

		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})
	results.AddEventListener("mouseup", false, func(event dom.Event) {
		mousedown = false
	})
	results.AddEventListener("mouseleave", false, func(event dom.Event) {
		mousedown = false
	})
	results.AddEventListener("mousemove", false, func(event dom.Event) {
		if !mousedown {
			return
		}

		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})*/
	results.AddEventListener("click", false, func(event dom.Event) {
		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})
	results.AddEventListener("dblclick", false, func(event dom.Event) {
		event.PreventDefault()

		hideOverlay(overlay)
	})

	overlay.AddEventListener("keydown", false, func(event dom.Event) {
		switch ke := event.(*dom.KeyboardEvent); {
		case ke.KeyIdentifier == "U+001B": // Escape.
			ke.PreventDefault()

			hideOverlay(overlay)

			if document.ActiveElement().Underlying() == command.Underlying() {
				//dom.GetWindow().Location().Hash = baseHash
				js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+baseHash)
				dom.GetWindow().ScrollTo(baseX, baseY)
			}
		case ke.KeyIdentifier == "Enter":
			ke.PreventDefault()

			hideOverlay(overlay)
		case ke.KeyIdentifier == "Down":
			ke.PreventDefault()

			switch {
			case !ke.CtrlKey && !ke.AltKey && ke.MetaKey:
				selected = len(entries) - 1
			case ke.CtrlKey && ke.AltKey && !ke.MetaKey:
				results.Underlying().Set("scrollTop", results.Underlying().Get("scrollTop").Float()+entryHeight)
				return
			case !ke.CtrlKey && !ke.AltKey && !ke.MetaKey:
				selected++
			}
			updateResultSelection()
		case ke.KeyIdentifier == "Up":
			ke.PreventDefault()

			switch {
			case !ke.CtrlKey && !ke.AltKey && ke.MetaKey:
				selected = 0
			case ke.CtrlKey && ke.AltKey && !ke.MetaKey:
				results.Underlying().Set("scrollTop", results.Underlying().Get("scrollTop").Float()-entryHeight)
				return
			case !ke.CtrlKey && !ke.AltKey && !ke.MetaKey:
				selected--
			}
			updateResultSelection()
		}
	})

	document.Body().AddEventListener("keydown", false, func(event dom.Event) {
		switch ke := event.(*dom.KeyboardEvent); {
		case ke.KeyIdentifier == "U+0052": // Cmd+R (or just R, since some browsers don't let us intercept Cmd+R).
			ke.PreventDefault()

			if display := overlay.Style().GetPropertyValue("display"); display != "none" && display != "null" {
				command.Select()
				break
			}

			command.Value = ""
			manuallyPicked = ""

			{
				headers = nil
				for _, header := range append(document.Body().GetElementsByTagName("h3"), document.Body().GetElementsByTagName("h4")...) {
					if header.ID() == "" {
						continue
					}
					headers = append(headers, header)
				}

				baseHash = strings.TrimPrefix(dom.GetWindow().Location().Hash, "#")
				baseX, baseY = dom.GetWindow().ScrollX(), dom.GetWindow().ScrollY()

				updateResults(true, overlay)
			}

			command.Select()
		case ke.KeyIdentifier == "U+001B": // Escape.
			ke.PreventDefault()

			hideOverlay(overlay)
		}
	})
}

var previouslyHighlightedHeader dom.HTMLElement

func hideOverlay(overlay dom.HTMLElement) {
	overlay.Style().SetProperty("display", "none", "")

	if previouslyHighlightedHeader != nil {
		previouslyHighlightedHeader.Class().Remove("highlighted")
		previouslyHighlightedHeader.Class().Add("highlighted-fade")
	}
}

var previouslySelected int

func updateResultSelection() {
	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	if selected < 0 {
		selected = 0
	} else if selected > len(entries)-1 {
		selected = len(entries) - 1
	}

	if selected == previouslySelected {
		return
	}

	entries[previouslySelected].(dom.Element).Class().Remove("gts-highlighted")
	if previouslyHighlightedHeader != nil {
		previouslyHighlightedHeader.Class().Remove("highlighted")
	}

	{
		element := entries[selected].(dom.Element)

		if element.GetBoundingClientRect().Top <= results.GetBoundingClientRect().Top {
			element.Underlying().Call("scrollIntoView", true)
		} else if element.GetBoundingClientRect().Bottom >= results.GetBoundingClientRect().Bottom {
			element.Underlying().Call("scrollIntoView", false)
		}

		element.Class().Add("gts-highlighted")
		//dom.GetWindow().Location().Hash = "#" + element.GetAttribute("data-id")
		//dom.GetWindow().History().ReplaceState(nil, nil, "#"+element.GetAttribute("data-id"))
		js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+element.GetAttribute("data-id"))
		target := document.GetElementByID(element.GetAttribute("data-id")).(dom.HTMLElement)
		target.Class().Add("highlighted")
		previouslyHighlightedHeader = target
		centerOnTargetIfOffscreen(target)

		manuallyPicked = element.GetAttribute("data-id")
	}

	previouslySelected = selected
}

func centerOnTargetIfOffscreen(target dom.HTMLElement) {
	isOffscreen := int(target.OffsetTop()) < dom.GetWindow().ScrollY() ||
		int(target.OffsetTop()+target.OffsetHeight()) > dom.GetWindow().ScrollY()+dom.GetWindow().InnerHeight()

	if isOffscreen {
		windowHalfHeight := dom.GetWindow().InnerHeight() / 2

		dom.GetWindow().ScrollTo(dom.GetWindow().ScrollX(), int(target.OffsetTop()+target.OffsetHeight())-windowHalfHeight)
	}
}

var initialSelected int

func updateResults(init bool, overlay dom.HTMLElement) {
	windowHalfHeight := dom.GetWindow().InnerHeight() / 2
	filter := document.GetElementByID("gts-command").(*dom.HTMLInputElement).Value

	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	var selectionPreserved = false

	results.SetInnerHTML("")
	var visibleIndex int
	for _, header := range headers {
		if filter != "" && !strings.Contains(strings.ToLower(header.TextContent()), strings.ToLower(filter)) {
			continue
		}

		element := document.CreateElement("div")
		element.Class().Add("gts-entry")
		element.SetAttribute("data-id", header.ID())
		{
			entry := header.TextContent()
			index := strings.Index(strings.ToLower(entry), strings.ToLower(filter))
			element.SetInnerHTML(html.EscapeString(entry[:index]) + "" + html.EscapeString(entry[index:index+len(filter)]) + "" + html.EscapeString(entry[index+len(filter):]))
		}
		if header.ID() == manuallyPicked {
			selectionPreserved = true

			selected = visibleIndex
			previouslySelected = visibleIndex
		}

		results.AppendChild(element)

		visibleIndex++
	}

	entries = results.ChildNodes()

	if !selectionPreserved {
		manuallyPicked = ""

		if init {
			// Find the nearest entry.
			for i := len(entries) - 1; i >= 0; i-- {
				element := entries[i].(dom.Element)
				header := document.GetElementByID(element.GetAttribute("data-id"))

				if header.GetBoundingClientRect().Top <= windowHalfHeight || i == 0 {
					selected = i
					previouslySelected = i

					initialSelected = i

					break
				}
			}
		} else {
			if filter == "" {
				selected = initialSelected
				previouslySelected = initialSelected
			} else {
				selected = 0
				previouslySelected = 0
			}
		}
	}

	if init {
		if previouslyHighlightedHeader != nil {
			previouslyHighlightedHeader.Class().Remove("highlighted-fade")
		}

		overlay.Style().SetProperty("display", "initial", "")
		entryHeight = results.FirstChild().(dom.Element).GetBoundingClientRect().Object.Get("height").Float()
	} else {
		if previouslyHighlightedHeader != nil {
			previouslyHighlightedHeader.Class().Remove("highlighted")
		}
	}

	if len(entries) > 0 {
		element := entries[selected].(dom.Element)

		if init {
			y := float64(selected) * entryHeight
			results.Underlying().Set("scrollTop", y-float64(results.GetBoundingClientRect().Height/2))
		} else {
			if element.GetBoundingClientRect().Top <= results.GetBoundingClientRect().Top {
				element.Underlying().Call("scrollIntoView", true)
			} else if element.GetBoundingClientRect().Bottom >= results.GetBoundingClientRect().Bottom {
				element.Underlying().Call("scrollIntoView", false)
			}
		}

		element.Class().Add("gts-highlighted")
		//dom.GetWindow().Location().Hash = "#" + element.GetAttribute("data-id")
		//dom.GetWindow().History().ReplaceState(nil, nil, "#"+element.GetAttribute("data-id"))
		js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+element.GetAttribute("data-id"))
		target := document.GetElementByID(element.GetAttribute("data-id")).(dom.HTMLElement)
		target.Class().Add("highlighted")
		previouslyHighlightedHeader = target
		centerOnTargetIfOffscreen(target)
	}
}
<commit_msg>Fix bug where typing R in command box would trigger Cmd+R event.<commit_after>// +build js

package main

import (
	"html"
	"strings"

	"github.com/gopherjs/gopherjs/js"

	"honnef.co/go/js/dom"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

var headers []dom.Element

var selected int

var baseHash string
var baseX, baseY int

var entryHeight float64
var entries []dom.Node
var manuallyPicked string

func main() {
	overlay := document.CreateElement("div").(*dom.HTMLDivElement)
	overlay.SetID("gts-overlay")

	container := document.CreateElement("div")
	overlay.AppendChild(container)
	container.Underlying().Set("outerHTML", `<div><input id="gts-command"></input><div id="gts-results"></div></div>`)

	document.Body().AppendChild(overlay)

	command := document.GetElementByID("gts-command").(*dom.HTMLInputElement)
	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	command.AddEventListener("input", false, func(event dom.Event) {
		updateResults(false, nil)
	})

	/*mousedown := false
	results.AddEventListener("mousedown", false, func(event dom.Event) {
		mousedown = true

		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})
	results.AddEventListener("mouseup", false, func(event dom.Event) {
		mousedown = false
	})
	results.AddEventListener("mouseleave", false, func(event dom.Event) {
		mousedown = false
	})
	results.AddEventListener("mousemove", false, func(event dom.Event) {
		if !mousedown {
			return
		}

		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})*/
	results.AddEventListener("click", false, func(event dom.Event) {
		command.Focus()

		me := event.(*dom.MouseEvent)
		y := (me.ClientY - results.GetBoundingClientRect().Top) + results.Underlying().Get("scrollTop").Int()
		selected = int(float64(y) / entryHeight)
		updateResultSelection()
	})
	results.AddEventListener("dblclick", false, func(event dom.Event) {
		event.PreventDefault()

		hideOverlay(overlay)
	})

	overlay.AddEventListener("keydown", false, func(event dom.Event) {
		switch ke := event.(*dom.KeyboardEvent); {
		case ke.KeyIdentifier == "U+001B": // Escape.
			ke.PreventDefault()

			hideOverlay(overlay)

			if document.ActiveElement().Underlying() == command.Underlying() {
				//dom.GetWindow().Location().Hash = baseHash
				js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+baseHash)
				dom.GetWindow().ScrollTo(baseX, baseY)
			}
		case ke.KeyIdentifier == "Enter":
			ke.PreventDefault()

			hideOverlay(overlay)
		case ke.KeyIdentifier == "Down":
			ke.PreventDefault()

			switch {
			case !ke.CtrlKey && !ke.AltKey && ke.MetaKey:
				selected = len(entries) - 1
			case ke.CtrlKey && ke.AltKey && !ke.MetaKey:
				results.Underlying().Set("scrollTop", results.Underlying().Get("scrollTop").Float()+entryHeight)
				return
			case !ke.CtrlKey && !ke.AltKey && !ke.MetaKey:
				selected++
			}
			updateResultSelection()
		case ke.KeyIdentifier == "Up":
			ke.PreventDefault()

			switch {
			case !ke.CtrlKey && !ke.AltKey && ke.MetaKey:
				selected = 0
			case ke.CtrlKey && ke.AltKey && !ke.MetaKey:
				results.Underlying().Set("scrollTop", results.Underlying().Get("scrollTop").Float()-entryHeight)
				return
			case !ke.CtrlKey && !ke.AltKey && !ke.MetaKey:
				selected--
			}
			updateResultSelection()
		}
	})

	document.Body().AddEventListener("keydown", false, func(event dom.Event) {
		switch ke := event.(*dom.KeyboardEvent); {
		case ke.KeyIdentifier == "U+0052": // Cmd+R (or just R, since some browsers don't let us intercept Cmd+R).
			// Ignore R when command elment has focus (it means the user is typing).
			if document.ActiveElement().Underlying() == command.Underlying() {
				break
			}

			ke.PreventDefault()

			if display := overlay.Style().GetPropertyValue("display"); display != "none" && display != "null" {
				command.Select()
				break
			}

			command.Value = ""
			manuallyPicked = ""

			{
				headers = nil
				for _, header := range append(document.Body().GetElementsByTagName("h3"), document.Body().GetElementsByTagName("h4")...) {
					if header.ID() == "" {
						continue
					}
					headers = append(headers, header)
				}

				baseHash = strings.TrimPrefix(dom.GetWindow().Location().Hash, "#")
				baseX, baseY = dom.GetWindow().ScrollX(), dom.GetWindow().ScrollY()

				updateResults(true, overlay)
			}

			command.Select()
		case ke.KeyIdentifier == "U+001B": // Escape.
			ke.PreventDefault()

			hideOverlay(overlay)
		}
	})
}

var previouslyHighlightedHeader dom.HTMLElement

func hideOverlay(overlay dom.HTMLElement) {
	overlay.Style().SetProperty("display", "none", "")

	if previouslyHighlightedHeader != nil {
		previouslyHighlightedHeader.Class().Remove("highlighted")
		previouslyHighlightedHeader.Class().Add("highlighted-fade")
	}
}

var previouslySelected int

func updateResultSelection() {
	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	if selected < 0 {
		selected = 0
	} else if selected > len(entries)-1 {
		selected = len(entries) - 1
	}

	if selected == previouslySelected {
		return
	}

	entries[previouslySelected].(dom.Element).Class().Remove("gts-highlighted")
	if previouslyHighlightedHeader != nil {
		previouslyHighlightedHeader.Class().Remove("highlighted")
	}

	{
		element := entries[selected].(dom.Element)

		if element.GetBoundingClientRect().Top <= results.GetBoundingClientRect().Top {
			element.Underlying().Call("scrollIntoView", true)
		} else if element.GetBoundingClientRect().Bottom >= results.GetBoundingClientRect().Bottom {
			element.Underlying().Call("scrollIntoView", false)
		}

		element.Class().Add("gts-highlighted")
		//dom.GetWindow().Location().Hash = "#" + element.GetAttribute("data-id")
		//dom.GetWindow().History().ReplaceState(nil, nil, "#"+element.GetAttribute("data-id"))
		js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+element.GetAttribute("data-id"))
		target := document.GetElementByID(element.GetAttribute("data-id")).(dom.HTMLElement)
		target.Class().Add("highlighted")
		previouslyHighlightedHeader = target
		centerOnTargetIfOffscreen(target)

		manuallyPicked = element.GetAttribute("data-id")
	}

	previouslySelected = selected
}

func centerOnTargetIfOffscreen(target dom.HTMLElement) {
	isOffscreen := int(target.OffsetTop()) < dom.GetWindow().ScrollY() ||
		int(target.OffsetTop()+target.OffsetHeight()) > dom.GetWindow().ScrollY()+dom.GetWindow().InnerHeight()

	if isOffscreen {
		windowHalfHeight := dom.GetWindow().InnerHeight() / 2

		dom.GetWindow().ScrollTo(dom.GetWindow().ScrollX(), int(target.OffsetTop()+target.OffsetHeight())-windowHalfHeight)
	}
}

var initialSelected int

func updateResults(init bool, overlay dom.HTMLElement) {
	windowHalfHeight := dom.GetWindow().InnerHeight() / 2
	filter := document.GetElementByID("gts-command").(*dom.HTMLInputElement).Value

	results := document.GetElementByID("gts-results").(*dom.HTMLDivElement)

	var selectionPreserved = false

	results.SetInnerHTML("")
	var visibleIndex int
	for _, header := range headers {
		if filter != "" && !strings.Contains(strings.ToLower(header.TextContent()), strings.ToLower(filter)) {
			continue
		}

		element := document.CreateElement("div")
		element.Class().Add("gts-entry")
		element.SetAttribute("data-id", header.ID())
		{
			entry := header.TextContent()
			index := strings.Index(strings.ToLower(entry), strings.ToLower(filter))
			element.SetInnerHTML(html.EscapeString(entry[:index]) + "" + html.EscapeString(entry[index:index+len(filter)]) + "" + html.EscapeString(entry[index+len(filter):]))
		}
		if header.ID() == manuallyPicked {
			selectionPreserved = true

			selected = visibleIndex
			previouslySelected = visibleIndex
		}

		results.AppendChild(element)

		visibleIndex++
	}

	entries = results.ChildNodes()

	if !selectionPreserved {
		manuallyPicked = ""

		if init {
			// Find the nearest entry.
			for i := len(entries) - 1; i >= 0; i-- {
				element := entries[i].(dom.Element)
				header := document.GetElementByID(element.GetAttribute("data-id"))

				if header.GetBoundingClientRect().Top <= windowHalfHeight || i == 0 {
					selected = i
					previouslySelected = i

					initialSelected = i

					break
				}
			}
		} else {
			if filter == "" {
				selected = initialSelected
				previouslySelected = initialSelected
			} else {
				selected = 0
				previouslySelected = 0
			}
		}
	}

	if init {
		if previouslyHighlightedHeader != nil {
			previouslyHighlightedHeader.Class().Remove("highlighted-fade")
		}

		overlay.Style().SetProperty("display", "initial", "")
		entryHeight = results.FirstChild().(dom.Element).GetBoundingClientRect().Object.Get("height").Float()
	} else {
		if previouslyHighlightedHeader != nil {
			previouslyHighlightedHeader.Class().Remove("highlighted")
		}
	}

	if len(entries) > 0 {
		element := entries[selected].(dom.Element)

		if init {
			y := float64(selected) * entryHeight
			results.Underlying().Set("scrollTop", y-float64(results.GetBoundingClientRect().Height/2))
		} else {
			if element.GetBoundingClientRect().Top <= results.GetBoundingClientRect().Top {
				element.Underlying().Call("scrollIntoView", true)
			} else if element.GetBoundingClientRect().Bottom >= results.GetBoundingClientRect().Bottom {
				element.Underlying().Call("scrollIntoView", false)
			}
		}

		element.Class().Add("gts-highlighted")
		//dom.GetWindow().Location().Hash = "#" + element.GetAttribute("data-id")
		//dom.GetWindow().History().ReplaceState(nil, nil, "#"+element.GetAttribute("data-id"))
		js.Global.Get("window").Get("history").Call("replaceState", nil, nil, "#"+element.GetAttribute("data-id"))
		target := document.GetElementByID(element.GetAttribute("data-id")).(dom.HTMLElement)
		target.Class().Add("highlighted")
		previouslyHighlightedHeader = target
		centerOnTargetIfOffscreen(target)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sync"
	"time"
)

type Record struct {
	t   time.Time
	key []byte
	val []byte
}

type RecordRef struct {
	seq    uint64
	offset uint64
}

type DB struct {
	// Immutable configuration (once the DB is constructed)
	chunkSize   uint64        // On-disk size limit for a chunk
	cacheChunks int           // Number of chunks to cache directly in memory
	expiry      time.Duration // How long to keep data around at all
	dir         string        // Where to keep database files

	// Overridable by test functions
	now   func() time.Time              // Called once on Put, for new records
	since func(time.Time) time.Duration // Called to check expiry, on Get and Rotate

	mu *sync.Mutex // Protects all of the following

	// Chunk files
	seq           uint64 // Current base sequence # for wchunk; seq+i+1 is the sequence # for an rchunk
	wchunk        *WriteChunk
	rchunks       []*ReadChunk
	rchunksCached int // wchunk is always cached, so this should be <= cacheChunks-1

	// Maps
	memCache map[string]*Record
	refCache map[string]*RecordRef

	closed bool
}

// NewDB creates a new DB with the given parameters. Dir must not already exist.
func NewDB(chunkSize uint64, cacheChunks int, expiry time.Duration, dir string) (*DB, error) {
	db := &DB{
		chunkSize:   chunkSize,
		cacheChunks: cacheChunks,
		expiry:      expiry,
		dir:         dir,

		now:   time.Now,
		since: time.Since,

		mu:       new(sync.Mutex),
		memCache: make(map[string]*Record),
		refCache: make(map[string]*RecordRef),
	}
	if err := os.Mkdir(dir, 0700); err != nil {
		return nil, err
	}
	wchunk, err := NewWriteChunk(db.wlogName(), db.chunkSize)
	if err != nil {
		return nil, err
	}
	db.wchunk = wchunk
	return db, nil
}

var (
	ErrKeyNotExist = errors.New("key does not exist in the database")
	ErrKeyExist    = errors.New("key already exists in the database")
	ErrDBClosed    = errors.New("database is closed")
)

func (db *DB) Get(k []byte) (v []byte, cached bool, err error) {
	db.mu.Lock()
	defer db.mu.Unlock()

	if db.closed {
		return nil, false, ErrDBClosed
	}

	s := string(k)
	if r, ok := db.memCache[s]; ok {
		if db.since(r.t) > db.expiry {
			return nil, false, ErrKeyNotExist
		}
		return r.val, true, nil
	}
	if ref, ok := db.refCache[s]; ok {
		rchunk := db.rchunkForSeq(ref.seq)
		r, err := rchunk.ReadRecord(ref.offset)
		if err != nil {
			return nil, false, err
		}
		if db.since(r.t) > db.expiry {
			return nil, false, ErrKeyNotExist
		}
		return r.val, false, nil
	}
	return nil, false, ErrKeyNotExist
}

func (db *DB) Put(k, v []byte) (rotated bool, err error) {
	db.mu.Lock()
	defer db.mu.Unlock()

	if db.closed {
		return false, ErrDBClosed
	}

	s := string(k)

	if _, ok := db.memCache[s]; ok {
		return rotated, ErrKeyExist
	}
	if _, ok := db.refCache[s]; ok {
		return rotated, ErrKeyExist
	}

	r := &Record{
		t:   db.now(),
		key: k,
		val: v,
	}
	offset, err := db.wchunk.WriteRecord(r)
	switch err {
	case ErrWriteLogFull:
		if err = db.Rotate(); err != nil {
			return rotated, err
		}
		rotated = true
		offset, err = db.wchunk.WriteRecord(r)
		if err != nil {
			return rotated, err
		}
	case nil:
	default:
		return rotated, err
	}
	db.memCache[s] = r
	db.refCache[s] = &RecordRef{seq: db.seq, offset: offset}
	return rotated, nil
}

func (db *DB) Close() error {
	db.mu.Lock()
	defer db.mu.Unlock()

	db.closed = true
	if err := db.wchunk.Close(); err != nil {
		return err
	}
	for _, rchunk := range db.rchunks {
		if err := rchunk.Close(); err != nil {
			return err
		}
	}
	return nil
}

// Rotate removes expired chunks, reopens the write log as a read log and adds it to the list, makes a fresh
// write log, and increments the sequence number.
func (db *DB) Rotate() error {
	log.Printf("sequence %d; rotating...", db.seq)
	// Add references to refCache.
	for _, entry := range db.wchunk.index {
		db.refCache[entry.key] = &RecordRef{
			seq:    db.seq,
			offset: entry.offset,
		}
	}
	// Rotate the chunk
	rchunk, err := db.wchunk.ReopenAsReadChunk()
	if err != nil {
		return err
	}
	db.rchunks = append([]*ReadChunk{rchunk}, db.rchunks...)
	db.seq++
	db.rchunksCached++
	db.wchunk, err = NewWriteChunk(db.wlogName(), db.chunkSize)
	if err != nil {
		return err
	}

	if err := db.removeExpiredChunks(); err != nil {
		return err
	}

	// If we have an extra cached chunk, remove it.
	if db.rchunksCached > db.cacheChunks-1 {
		if db.rchunksCached != db.cacheChunks {
			panic("too many chunks cached")
		}
		for _, entry := range db.rchunks[db.rchunksCached-1].index {
			delete(db.memCache, entry.key)
		}
		db.rchunksCached--
	}
	return nil
}

func (db *DB) removeExpiredChunks() error {
	for i := len(db.rchunks) - 1; i >= 0; i-- {
		rchunk := db.rchunks[i]
		// Check whether this whole chunk is expired by looking at the most recent timestamp.
		if db.since(rchunk.lastTimestamp) <= db.expiry {
			break
		}
		// Remove the log file.
		if err := rchunk.Close(); err != nil {
			return err
		}
		if err := os.Remove(rchunk.Filename()); err != nil {
			return err
		}
		db.rchunks = db.rchunks[:i]
		// Remove the entries in refCache and memCache as well as the index.
		cached := i < db.rchunksCached
		for _, entry := range rchunk.index {
			delete(db.refCache, entry.key)
			if cached {
				delete(db.memCache, entry.key)
			}
		}
		if cached {
			db.rchunksCached--
		}
	}
	return nil
}

func (db *DB) wlogName() string {
	return filepath.Join(db.dir, fmt.Sprintf("chunk%010d", db.seq))
}

func (db *DB) rchunkForSeq(seq uint64) *ReadChunk {
	return db.rchunks[int(db.seq-seq-1)]
}
<commit_msg>Start splitting out OpenDB function<commit_after>package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sync"
	"time"
)

type Record struct {
	t   time.Time
	key []byte
	val []byte
}

type RecordRef struct {
	seq    uint64
	offset uint64
}

type DB struct {
	// Immutable configuration (once the DB is constructed)
	chunkSize   uint64        // On-disk size limit for a chunk
	cacheChunks int           // Number of chunks to cache directly in memory
	expiry      time.Duration // How long to keep data around at all
	dir         string        // Where to keep database files

	// Overridable by test functions
	now   func() time.Time              // Called once on Put, for new records
	since func(time.Time) time.Duration // Called to check expiry, on Get and Rotate

	mu *sync.Mutex // Protects all of the following

	// Chunk files
	seq           uint64 // Current base sequence # for wchunk; seq+i+1 is the sequence # for an rchunk
	wchunk        *WriteChunk
	rchunks       []*ReadChunk
	rchunksCached int // wchunk is always cached, so this should be <= cacheChunks-1

	// Maps
	memCache map[string]*Record
	refCache map[string]*RecordRef

	closed bool
}

func newDB(chunkSize uint64, cacheChunks int, expiry time.Duration, dir string) *DB {
	return &DB{
		chunkSize:   chunkSize,
		cacheChunks: cacheChunks,
		expiry:      expiry,
		dir:         dir,

		now:   time.Now,
		since: time.Since,

		mu:       new(sync.Mutex),
		memCache: make(map[string]*Record),
		refCache: make(map[string]*RecordRef),
	}
}

// NewDB creates a new DB with the given parameters. Dir must not already exist.
func NewDB(chunkSize uint64, cacheChunks int, expiry time.Duration, dir string) (*DB, error) {
	db := newDB(chunkSize, cacheChunks, expiry, dir)
	if err := os.Mkdir(dir, 0700); err != nil {
		return nil, err
	}
	wchunk, err := NewWriteChunk(db.wlogName(), db.chunkSize)
	if err != nil {
		return nil, err
	}
	db.wchunk = wchunk
	return db, nil
}

func OpenDB(chunkSize uint64, cacheChunks int, expiry time.Duration, dir string) (*DB, error) {
	db := newDB(chunkSize, cacheChunks, expiry, dir)
	panic("unimplemented")
}

var (
	ErrKeyNotExist = errors.New("key does not exist in the database")
	ErrKeyExist    = errors.New("key already exists in the database")
	ErrDBClosed    = errors.New("database is closed")
)

func (db *DB) Get(k []byte) (v []byte, cached bool, err error) {
	db.mu.Lock()
	defer db.mu.Unlock()

	if db.closed {
		return nil, false, ErrDBClosed
	}

	s := string(k)
	if r, ok := db.memCache[s]; ok {
		if db.since(r.t) > db.expiry {
			return nil, false, ErrKeyNotExist
		}
		return r.val, true, nil
	}
	if ref, ok := db.refCache[s]; ok {
		rchunk := db.rchunkForSeq(ref.seq)
		r, err := rchunk.ReadRecord(ref.offset)
		if err != nil {
			return nil, false, err
		}
		if db.since(r.t) > db.expiry {
			return nil, false, ErrKeyNotExist
		}
		return r.val, false, nil
	}
	return nil, false, ErrKeyNotExist
}

func (db *DB) Put(k, v []byte) (rotated bool, err error) {
	db.mu.Lock()
	defer db.mu.Unlock()
	if db.closed {
		return false, ErrDBClosed
	}

	s := string(k)

	if _, ok := db.memCache[s]; ok {
		return rotated, ErrKeyExist
	}
	if _, ok := db.refCache[s]; ok {
		return rotated, ErrKeyExist
	}

	r := &Record{
		t:   db.now(),
		key: k,
		val: v,
	}
	offset, err := db.wchunk.WriteRecord(r)
	switch err {
	case ErrWriteLogFull:
		if err = db.Rotate(); err != nil {
			return rotated, err
		}
		rotated = true
		offset, err = db.wchunk.WriteRecord(r)
		if err != nil {
			return rotated, err
		}
	case nil:
	default:
		return rotated, err
	}
	db.memCache[s] = r
	db.refCache[s] = &RecordRef{seq: db.seq, offset: offset}
	return rotated, nil
}

func (db *DB) Close() error {
	db.mu.Lock()
	defer db.mu.Unlock()

	db.closed = true
	if err := db.wchunk.Close(); err != nil {
		return err
	}
	for _, rchunk := range db.rchunks {
		if err := rchunk.Close(); err != nil {
			return err
		}
	}
	return nil
}

// Rotate removes expired chunks, reopens the write log as a read log and adds it to the list, makes a fresh
// write log, and increments the sequence number.
func (db *DB) Rotate() error {
	log.Printf("sequence %d; rotating...", db.seq)
	// Add references to refCache.
	for _, entry := range db.wchunk.index {
		db.refCache[entry.key] = &RecordRef{
			seq:    db.seq,
			offset: entry.offset,
		}
	}
	// Rotate the chunk
	rchunk, err := db.wchunk.ReopenAsReadChunk()
	if err != nil {
		return err
	}
	db.rchunks = append([]*ReadChunk{rchunk}, db.rchunks...)
	db.seq++
	db.rchunksCached++
	db.wchunk, err = NewWriteChunk(db.wlogName(), db.chunkSize)
	if err != nil {
		return err
	}

	if err := db.removeExpiredChunks(); err != nil {
		return err
	}

	// If we have an extra cached chunk, remove it.
	if db.rchunksCached > db.cacheChunks-1 {
		if db.rchunksCached != db.cacheChunks {
			panic("too many chunks cached")
		}
		for _, entry := range db.rchunks[db.rchunksCached-1].index {
			delete(db.memCache, entry.key)
		}
		db.rchunksCached--
	}
	return nil
}

func (db *DB) removeExpiredChunks() error {
	for i := len(db.rchunks) - 1; i >= 0; i-- {
		rchunk := db.rchunks[i]
		// Check whether this whole chunk is expired by looking at the most recent timestamp.
		if db.since(rchunk.lastTimestamp) <= db.expiry {
			break
		}
		// Remove the log file.
		if err := rchunk.Close(); err != nil {
			return err
		}
		if err := os.Remove(rchunk.Filename()); err != nil {
			return err
		}
		db.rchunks = db.rchunks[:i]
		// Remove the entries in refCache and memCache as well as the index.
		cached := i < db.rchunksCached
		for _, entry := range rchunk.index {
			delete(db.refCache, entry.key)
			if cached {
				delete(db.memCache, entry.key)
			}
		}
		if cached {
			db.rchunksCached--
		}
	}
	return nil
}

func (db *DB) wlogName() string {
	return filepath.Join(db.dir, fmt.Sprintf("chunk%010d", db.seq))
}

func (db *DB) rchunkForSeq(seq uint64) *ReadChunk {
	return db.rchunks[int(db.seq-seq-1)]
}
<|endoftext|>


<commit_before>package ezdb

import (
	"errors"
	"fmt"
	"os"
	"strconv"

	"github.com/jmhodges/levigo"
)

var (
	// DEBUG is debug mode.
	// Set this flag to true to output debug messages from this package.
	DEBUG                              = false
	DefCacheSize           int         = 1024 * 1024 * 16     // Default leveldb cache size.
	DefDBFolderPermission  os.FileMode = 0755                 // Default database dir permission
	ERR_KEY_DOES_NOT_EXIST             = "key does not exist" // Key not exists error message.
)

// DB is a wrapper of levigo.DB.
type DB struct {
	LevigoDB *levigo.DB           // Instance of levigo.DB
	ro       *levigo.ReadOptions  // Read options for Get() of leveldb.
	roIt     *levigo.ReadOptions  // Read options for itarators of leveldb.
	wo       *levigo.WriteOptions // Write options for Put() of leveldb.
	cache    *levigo.Cache        // Cache of leveldb.
}

// GoThroughProcessor provides the interface to process leveldb record while go through the leveldb database.
type GoThroughProcessor interface {
	Process(k, v string) error
}

// Open opens a leveldb database.
func Open(dbPath string, cacheSize int) (db *DB, err error) {
	db = new(DB)

	if DEBUG {
		fmt.Printf("Open(): dbPath = %v, cacheSize = %v\n", dbPath, cacheSize)
	}

	if db.cache = levigo.NewLRUCache(cacheSize); db.cache == nil {
		err = errors.New("levigo.NewLRUCache() == nil")
		if DEBUG {
			fmt.Println(err)
		}
		return nil, err
	}
	opts := levigo.NewOptions()
	opts.SetCache(db.cache)
	opts.SetCreateIfMissing(true)

	if err = os.MkdirAll(dbPath, DefDBFolderPermission); err != nil {
		if DEBUG {
			fmt.Printf("os.MkDirAll(%v, %v) err: %v\n", dbPath, DefDBFolderPermission, err)
		}
		return nil, err
	}

	if db.LevigoDB, err = levigo.Open(dbPath, opts); err != nil {
		if DEBUG {
			fmt.Println(err)
		}
		return nil, err
	}

	db.ro = levigo.NewReadOptions()
	db.roIt = levigo.NewReadOptions()
	db.roIt.SetFillCache(false)
	db.wo = levigo.NewWriteOptions()

	return db, err
}

// Close closes the leveldb database after use.
func (db *DB) Close() {
	if db == nil {
		return
	}

	if db.roIt != nil {
		db.roIt.Close()
	}

	if db.ro != nil {
		db.ro.Close()
	}

	if db.wo != nil {
		db.wo.Close()
	}

	if db.LevigoDB != nil {
		db.LevigoDB.Close()
	}
	// delete cache AFTER close leveldb or it will hang.
	// See cache in http://leveldb.googlecode.com/svn/trunk/doc/index.html
	if db.cache != nil {
		db.cache.Close()
	}
}

// Put is a wrapper for levigo.DB.Put().
func (db *DB) Put(key, value []byte) (err error) {
	return db.LevigoDB.Put(db.wo, key, value)
}

// Get is a wrapper for levigo.DB.Get().
func (db *DB) Get(key []byte) (value []byte, err error) {
	return db.LevigoDB.Get(db.ro, key)
}

// Delete is a wrapper for levigo.DB.Delete()
func (db *DB) Delete(key []byte) (err error) {
	return db.LevigoDB.Delete(db.wo, key)
}

// PutStr puts the key / value as string value.
func (db *DB) PutStr(key, value string) (err error) {
	return db.Put([]byte(key), []byte(value))
}

// KeyExist checks if key exists or not.
func (db *DB) KeyExist(key string) (exist bool, err error) {
	v, err := db.Get([]byte(key))
	if err != nil {
		return false, err
	}

	if v == nil {
		return false, nil
	}
	return true, nil
}

// GetStr gets the key / value as string value.
func (db *DB) GetStr(key string) (value string, err error) {
	v, err := db.Get([]byte(key))
	if v == nil {
		return "", errors.New(ERR_KEY_DOES_NOT_EXIST)
	}
	return string(v), err
}

// PutInt64 stores int64 as string in db. It should be used with Getint64().
func (db *DB) PutInt64(key string, value int64) (err error) {
	s := strconv.FormatInt(value, 10)
	return db.PutStr(key, s)
}

// GetInt64 get string value and convert it to int64. It should be used with PutInt64().
func (db *DB) GetInt64(key string) (value int64, err error) {
	s, err := db.GetStr(key)
	if err != nil {
		return 0, err
	}
	return strconv.ParseInt(s, 10, 64)
}

// PutUint64 store uint64 as string in db. It should be used with GetUint64().
func (db *DB) PutUint64(key string, value uint64) (err error) {
	s := strconv.FormatUint(value, 10)
	return db.PutStr(key, s)
}

// GetUint64 get string value and convert it to uint64. It should be used with PutUInt64().
func (db *DB) GetUint64(key string) (value uint64, err error) {
	s, err := db.GetStr(key)
	if err != nil {
		return 0, err
	}
	return strconv.ParseUint(s, 10, 64)
}

// DeleteStr delete the string key.
func (db *DB) DeleteStr(key string) (err error) {
	return db.Delete([]byte(key))
}

// NewIterator creates a new iterator of levigo.
func (db *DB) NewIterator() *levigo.Iterator {
	return db.LevigoDB.NewIterator(db.roIt)
}

// IsIteratorValidForGoThrough checks if current iterator is valid while go through the db.
func IsIteratorValidForGoThrough(it *levigo.Iterator, keyEnd string) bool {
	if keyEnd != "" {
		return it.Valid() && string(it.Key()) <= keyEnd
	}
	return it.Valid()
}

// GoThrough goes through the leveldb db and call the GoThroughProcessor.Process() to process data.
func (db *DB) GoThrough(keyStart, keyEnd string, processor GoThroughProcessor) (err error) {
	it := db.NewIterator()
	defer it.Close()

	if keyStart != "" {
		it.Seek([]byte(keyStart))
	} else {
		it.SeekToFirst()
	}

	k := ""
	v := ""
	for ; IsIteratorValidForGoThrough(it, keyEnd); it.Next() {
		k = string(it.Key())
		v = string(it.Value())
		if err = processor.Process(k, v); err != nil {
			if DEBUG {
				fmt.Printf("processor.Process(%v, %v) err: %v\n", k, v, err)
			}
			return err
		}
	}

	if err := it.GetError(); err != nil {
		if DEBUG {
			fmt.Printf("it.GetError(): %s\n", err)
		}
		return err
	}

	return nil
}
<commit_msg>Unexport constants to remove golint warnings.<commit_after>package ezdb

import (
	"errors"
	"fmt"
	"os"
	"strconv"

	"github.com/jmhodges/levigo"
)

var (
	// DEBUG is debug mode.
	// Set this flag to true to output debug messages from this package.
	DEBUG                             = false
	defCacheSize          int         = 1024 * 1024 * 16     // Default leveldb cache size.
	defDBFolderPermission os.FileMode = 0755                 // Default database dir permission
	errKeyNotExists                   = "key does not exist" // Key not exists error message.
)

// DB is a wrapper of levigo.DB.
type DB struct {
	LevigoDB *levigo.DB           // Instance of levigo.DB
	ro       *levigo.ReadOptions  // Read options for Get() of leveldb.
	roIt     *levigo.ReadOptions  // Read options for itarators of leveldb.
	wo       *levigo.WriteOptions // Write options for Put() of leveldb.
	cache    *levigo.Cache        // Cache of leveldb.
}

// GoThroughProcessor provides the interface to process leveldb record while go through the leveldb database.
type GoThroughProcessor interface {
	Process(k, v string) error
}

// Open opens a leveldb database.
func Open(dbPath string, cacheSize int) (db *DB, err error) {
	db = new(DB)

	if DEBUG {
		fmt.Printf("Open(): dbPath = %v, cacheSize = %v\n", dbPath, cacheSize)
	}

	if db.cache = levigo.NewLRUCache(cacheSize); db.cache == nil {
		err = errors.New("levigo.NewLRUCache() == nil")
		if DEBUG {
			fmt.Println(err)
		}
		return nil, err
	}
	opts := levigo.NewOptions()
	opts.SetCache(db.cache)
	opts.SetCreateIfMissing(true)

	if err = os.MkdirAll(dbPath, defDBFolderPermission); err != nil {
		if DEBUG {
			fmt.Printf("os.MkDirAll(%v, %v) err: %v\n", dbPath, defDBFolderPermission, err)
		}
		return nil, err
	}

	if db.LevigoDB, err = levigo.Open(dbPath, opts); err != nil {
		if DEBUG {
			fmt.Println(err)
		}
		return nil, err
	}

	db.ro = levigo.NewReadOptions()
	db.roIt = levigo.NewReadOptions()
	db.roIt.SetFillCache(false)
	db.wo = levigo.NewWriteOptions()

	return db, err
}

// Close closes the leveldb database after use.
func (db *DB) Close() {
	if db == nil {
		return
	}

	if db.roIt != nil {
		db.roIt.Close()
	}

	if db.ro != nil {
		db.ro.Close()
	}

	if db.wo != nil {
		db.wo.Close()
	}

	if db.LevigoDB != nil {
		db.LevigoDB.Close()
	}
	// delete cache AFTER close leveldb or it will hang.
	// See cache in http://leveldb.googlecode.com/svn/trunk/doc/index.html
	if db.cache != nil {
		db.cache.Close()
	}
}

// Put is a wrapper for levigo.DB.Put().
func (db *DB) Put(key, value []byte) (err error) {
	return db.LevigoDB.Put(db.wo, key, value)
}

// Get is a wrapper for levigo.DB.Get().
func (db *DB) Get(key []byte) (value []byte, err error) {
	return db.LevigoDB.Get(db.ro, key)
}

// Delete is a wrapper for levigo.DB.Delete()
func (db *DB) Delete(key []byte) (err error) {
	return db.LevigoDB.Delete(db.wo, key)
}

// PutStr puts the key / value as string value.
func (db *DB) PutStr(key, value string) (err error) {
	return db.Put([]byte(key), []byte(value))
}

// KeyExist checks if key exists or not.
func (db *DB) KeyExist(key string) (exist bool, err error) {
	v, err := db.Get([]byte(key))
	if err != nil {
		return false, err
	}

	if v == nil {
		return false, nil
	}
	return true, nil
}

// GetStr gets the key / value as string value.
func (db *DB) GetStr(key string) (value string, err error) {
	v, err := db.Get([]byte(key))
	if v == nil {
		return "", errors.New(errKeyNotExists)
	}
	return string(v), err
}

// PutInt64 stores int64 as string in db. It should be used with Getint64().
func (db *DB) PutInt64(key string, value int64) (err error) {
	s := strconv.FormatInt(value, 10)
	return db.PutStr(key, s)
}

// GetInt64 get string value and convert it to int64. It should be used with PutInt64().
func (db *DB) GetInt64(key string) (value int64, err error) {
	s, err := db.GetStr(key)
	if err != nil {
		return 0, err
	}
	return strconv.ParseInt(s, 10, 64)
}

// PutUint64 store uint64 as string in db. It should be used with GetUint64().
func (db *DB) PutUint64(key string, value uint64) (err error) {
	s := strconv.FormatUint(value, 10)
	return db.PutStr(key, s)
}

// GetUint64 get string value and convert it to uint64. It should be used with PutUInt64().
func (db *DB) GetUint64(key string) (value uint64, err error) {
	s, err := db.GetStr(key)
	if err != nil {
		return 0, err
	}
	return strconv.ParseUint(s, 10, 64)
}

// DeleteStr delete the string key.
func (db *DB) DeleteStr(key string) (err error) {
	return db.Delete([]byte(key))
}

// NewIterator creates a new iterator of levigo.
func (db *DB) NewIterator() *levigo.Iterator {
	return db.LevigoDB.NewIterator(db.roIt)
}

// IsIteratorValidForGoThrough checks if current iterator is valid while go through the db.
func IsIteratorValidForGoThrough(it *levigo.Iterator, keyEnd string) bool {
	if keyEnd != "" {
		return it.Valid() && string(it.Key()) <= keyEnd
	}
	return it.Valid()
}

// GoThrough goes through the leveldb db and call the GoThroughProcessor.Process() to process data.
func (db *DB) GoThrough(keyStart, keyEnd string, processor GoThroughProcessor) (err error) {
	it := db.NewIterator()
	defer it.Close()

	if keyStart != "" {
		it.Seek([]byte(keyStart))
	} else {
		it.SeekToFirst()
	}

	k := ""
	v := ""
	for ; IsIteratorValidForGoThrough(it, keyEnd); it.Next() {
		k = string(it.Key())
		v = string(it.Value())
		if err = processor.Process(k, v); err != nil {
			if DEBUG {
				fmt.Printf("processor.Process(%v, %v) err: %v\n", k, v, err)
			}
			return err
		}
	}

	if err := it.GetError(); err != nil {
		if DEBUG {
			fmt.Printf("it.GetError(): %s\n", err)
		}
		return err
	}

	return nil
}
<|endoftext|>


<commit_before>package invoices

import (
	"log"
	"os"

	"github.com/jinzhu/gorm"
	"github.com/shyang107/go-twinvoices/util"
	// use for sqlite
	_ "github.com/jinzhu/gorm/dialects/sqlite"
)

// DB is database
var DB *gorm.DB

// Initialdb initialize database
func Initialdb() error {
	util.DebugPrintCaller()
	// util.Verbose = true
	// if util.IsFileExist(Cfg.DBfilename) {
	// 	// Pstat("  > Removing file %q ...\n", Cfg.DBfilename)
	// 	Glog.Infof("☞  Removing file %q ...\n", Cfg.DBfilename)
	// 	err := os.Remove(Cfg.DBfilename)
	// 	if err != nil {
	// 		// panic(err)
	// 		return err
	// 	}
	// }
	fl := os.ExpandEnv(Cfg.DBfilename)
	_, err := os.Stat(fl)
	Glog.Debug(err)
	if !os.IsNotExist(err) {
		Glog.Infof("♲  Removing file %q ...\n", fl)
		err := os.RemoveAll(fl)
		if err != nil {
			// panic(err)
			Glog.Error(err)
			return err
		}
	}
	// Pstat("  > Creating file %q ...\n", Cfg.DBfilename)
	Glog.Infof("♲  Creating file %q ...\n", fl)
	db, err := gorm.Open("sqlite3", fl)
	if err != nil {
		// Panic("failed to connect database")
		return err
	}
	defer db.Close()
	//
	// Migrate the schema
	db.AutoMigrate(&Invoice{}, &Detail{}, &FileBunker{})
	db.Model(&Invoice{}).Related(&Detail{}, "uin")
	// db.Model(&Invoice{}).AddUniqueIndex("idx_invoices_number", "uin")
	// db.Model(&Invoice{}).AddForeignKey("uin", "details(id)", "RESTRICT", "RESTRICT")
	return nil
}

// Connectdb connect the database
func Connectdb() {
	// glDebugf("* %q called by %q", util.CallerName(1), util.CallerName(2))
	util.DebugPrintCaller()
	//初始化并保持连接
	var err error
	DB, err = gorm.Open("sqlite3", Cfg.DBfilename)
	//    DB.LogMode(true)//打印sql语句
	if err != nil {
		log.Fatalf("☠  Database connect is err: %s", err.Error())
	} else {
		// log.Print("connect database is success")
		// Pinfo("* connect database is success\n")
		Glog.Debugf("♲  Connect database is success")
	}
	err = DB.DB().Ping()
	if err != nil {
		DB.DB().Close()
		log.Fatalf("☠  Error on opening database connection: %s", err.Error())
	}
	DB.Model(&Invoice{}).Related(&Detail{}, "uin")
}

// dbGetAllInvoices get the list from database
func dbGetAllInvoices() (*InvoiceCollection, error) {
	util.DebugPrintCaller()
	invs := []*Invoice{}
	DB.Find(&invs)
	for _, p := range invs {
		DB.Model(&p).Association("details").Find(&p.Details)
	}
	var res InvoiceCollection = invs
	return &res, nil
}

// dbInsertFrom creats records from []*Invoice into database
func dbInsertFrom(pvs []*Invoice) {
	cacheMu.Lock()
	defer cacheMu.Unlock()

	Glog.Infof("♲  Updating database ...")
	util.DebugPrintCaller()
	for _, v := range pvs {
		// io.Pforan("# %v", *v)
		// DB.FirstOrCreate(v)
		DB.Where(Invoice{UINumber: v.UINumber}).FirstOrCreate(v)
	}
}

// DBDumpData dumps all data from db
func DBDumpData(dumpFilename string) error {
	util.DebugPrintCaller()
	// Prun("  > Dumping data from %q ...\n", Cfg.DBfilename)
	Glog.Infof("♲  Dumping data from %q ...", Cfg.DBfilename)
	pvs, err := dbGetAllInvoices()
	if err != nil {
		return err
	}

	// return dbWriteInvoices(pvs, dumpFilename)
	out := &OutputFile{
		Filename: os.ExpandEnv(dumpFilename),
		Suffix:   util.FnExt(dumpFilename),
		IsOutput: true,
	}

	return out.WriteInvoices(pvs)
}
<commit_msg>Add something about new ListCommand()<commit_after>package invoices

import (
	"log"
	"os"

	"github.com/jinzhu/gorm"
	"github.com/shyang107/go-twinvoices/util"
	// use for sqlite
	_ "github.com/jinzhu/gorm/dialects/sqlite"
)

// DB is database
var DB *gorm.DB

// Initialdb initialize database
func Initialdb() error {
	util.DebugPrintCaller()
	// util.Verbose = true
	// if util.IsFileExist(Cfg.DBfilename) {
	// 	// Pstat("  > Removing file %q ...\n", Cfg.DBfilename)
	// 	Glog.Infof("☞  Removing file %q ...\n", Cfg.DBfilename)
	// 	err := os.Remove(Cfg.DBfilename)
	// 	if err != nil {
	// 		// panic(err)
	// 		return err
	// 	}
	// }
	fl := os.ExpandEnv(Cfg.DBfilename)
	_, err := os.Stat(fl)
	Glog.Debug(err)
	if !os.IsNotExist(err) {
		Glog.Infof("♲  Removing file %q ...\n", fl)
		err := os.RemoveAll(fl)
		if err != nil {
			// panic(err)
			Glog.Error(err)
			return err
		}
	}
	// Pstat("  > Creating file %q ...\n", Cfg.DBfilename)
	Glog.Infof("♲  Creating file %q ...\n", fl)
	db, err := gorm.Open("sqlite3", fl)
	if err != nil {
		// Panic("failed to connect database")
		return err
	}
	defer db.Close()
	//
	// Migrate the schema
	db.AutoMigrate(&Invoice{}, &Detail{}, &FileBunker{})
	db.Model(&Invoice{}).Related(&Detail{}, "uin")
	// db.Model(&Invoice{}).AddUniqueIndex("idx_invoices_number", "uin")
	// db.Model(&Invoice{}).AddForeignKey("uin", "details(id)", "RESTRICT", "RESTRICT")
	return nil
}

// Connectdb connect the database
func Connectdb() {
	// glDebugf("* %q called by %q", util.CallerName(1), util.CallerName(2))
	util.DebugPrintCaller()
	//初始化并保持连接
	var err error
	DB, err = gorm.Open("sqlite3", Cfg.DBfilename)
	//    DB.LogMode(true)//打印sql语句
	if err != nil {
		log.Fatalf("☠  Database connect is err: %s", err.Error())
	} else {
		// log.Print("connect database is success")
		// Pinfo("* connect database is success\n")
		Glog.Debugf("♲  Connect database is success")
	}
	err = DB.DB().Ping()
	if err != nil {
		DB.DB().Close()
		log.Fatalf("☠  Error on opening database connection: %s", err.Error())
	}
	DB.Model(&Invoice{}).Related(&Detail{}, "uin")
}

// DBGetAllInvoices get the list from database
func DBGetAllInvoices() (*InvoiceCollection, error) {
	util.DebugPrintCaller()
	invs := []*Invoice{}
	DB.Find(&invs)
	for _, p := range invs {
		DB.Model(&p).Association("details").Find(&p.Details)
	}
	var res InvoiceCollection = invs
	return &res, nil
}

// dbInsertFrom creats records from []*Invoice into database
func dbInsertFrom(pvs []*Invoice) {
	cacheMu.Lock()
	defer cacheMu.Unlock()

	Glog.Infof("♲  Updating database ...")
	util.DebugPrintCaller()
	for _, v := range pvs {
		// io.Pforan("# %v", *v)
		// DB.FirstOrCreate(v)
		DB.Where(Invoice{UINumber: v.UINumber}).FirstOrCreate(v)
	}
}

// DBDumpData dumps all data from db
func DBDumpData(dumpFilename string) error {
	util.DebugPrintCaller()
	// Prun("  > Dumping data from %q ...\n", Cfg.DBfilename)
	Glog.Infof("♲  Dumping data from %q ...", Cfg.DBfilename)
	pvs, err := DBGetAllInvoices()
	if err != nil {
		return err
	}

	// return dbWriteInvoices(pvs, dumpFilename)
	out := &OutputFile{
		Filename: os.ExpandEnv(dumpFilename),
		Suffix:   util.FnExt(dumpFilename),
		IsOutput: true,
	}

	return out.WriteInvoices(pvs)
}
<|endoftext|>


<commit_before>// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ca

import (
	"fmt"
	"time"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/peer"
	"google.golang.org/grpc/status"

	pb "istio.io/api/security/v1alpha1"
	"istio.io/istio/pkg/security"
	"istio.io/istio/security/pkg/pki/ca"
	caerror "istio.io/istio/security/pkg/pki/error"
	"istio.io/istio/security/pkg/pki/util"
	"istio.io/pkg/log"
)

var serverCaLog = log.RegisterScope("serverca", "Citadel server log", 0)

// CertificateAuthority contains methods to be supported by a CA.
type CertificateAuthority interface {
	// Sign generates a certificate for a workload or CA, from the given CSR and cert opts.
	Sign(csrPEM []byte, opts ca.CertOpts) ([]byte, error)
	// SignWithCertChain is similar to Sign but returns the leaf cert and the entire cert chain.
	SignWithCertChain(csrPEM []byte, opts ca.CertOpts) ([]string, error)
	// SignWithCertChain is similar to Sign but returns the leaf cert and the entire cert chain.
	GetCAKeyCertBundle() *util.KeyCertBundle
}

// Server implements IstioCAService and IstioCertificateService and provides the services on the
// specified port.
type Server struct {
	monitoring     monitoringMetrics
	Authenticators []security.Authenticator
	ca             CertificateAuthority
	serverCertTTL  time.Duration
}

func getConnectionAddress(ctx context.Context) string {
	peerInfo, ok := peer.FromContext(ctx)
	peerAddr := "unknown"
	if ok {
		peerAddr = peerInfo.Addr.String()
	}
	return peerAddr
}

// CreateCertificate handles an incoming certificate signing request (CSR). It does
// authentication and authorization. Upon validated, signs a certificate that:
// the SAN is the identity of the caller in authentication result.
// the subject public key is the public key in the CSR.
// the validity duration is the ValidityDuration in request, or default value if the given duration is invalid.
// it is signed by the CA signing key.
func (s *Server) CreateCertificate(ctx context.Context, request *pb.IstioCertificateRequest) (
	*pb.IstioCertificateResponse, error) {
	s.monitoring.CSR.Increment()
	caller := Authenticate(ctx, s.Authenticators)
	if caller == nil {
		s.monitoring.AuthnError.Increment()
		return nil, status.Error(codes.Unauthenticated, "request authenticate failure")
	}
	// TODO: Call authorizer.
	crMetadata := request.Metadata.GetFields()
	certSigner := crMetadata[security.CertSigner].GetStringValue()
	log.Debugf("cert signer from workload %s", certSigner)
	_, _, certChainBytes, rootCertBytes := s.ca.GetCAKeyCertBundle().GetAll()
	certOpts := ca.CertOpts{
		SubjectIDs: caller.Identities,
		TTL:        time.Duration(request.ValidityDuration) * time.Second,
		ForCA:      false,
		CertSigner: certSigner,
	}
	var signErr error
	var cert []byte
	var respCertChain []string
	if certSigner == "" {
		cert, signErr = s.ca.Sign([]byte(request.Csr), certOpts)
	} else {
		respCertChain, signErr = s.ca.SignWithCertChain([]byte(request.Csr), certOpts)
	}
	if signErr != nil {
		serverCaLog.Errorf("CSR signing error (%v)", signErr.Error())
		s.monitoring.GetCertSignError(signErr.(*caerror.Error).ErrorType()).Increment()
		return nil, status.Errorf(signErr.(*caerror.Error).HTTPErrorCode(), "CSR signing error (%v)", signErr.(*caerror.Error))
	}
	if certSigner == "" {
		respCertChain = []string{string(cert)}
		if len(certChainBytes) != 0 {
			respCertChain = append(respCertChain, string(certChainBytes))
		}
	}
	respCertChain = append(respCertChain, string(rootCertBytes))
	response := &pb.IstioCertificateResponse{
		CertChain: respCertChain,
	}
	s.monitoring.Success.Increment()
	serverCaLog.Debug("CSR successfully signed.")
	return response, nil
}

func recordCertsExpiry(keyCertBundle *util.KeyCertBundle) {
	rootCertExpiry, err := keyCertBundle.ExtractRootCertExpiryTimestamp()
	if err != nil {
		serverCaLog.Errorf("failed to extract root cert expiry timestamp (error %v)", err)
	}
	rootCertExpiryTimestamp.Record(rootCertExpiry)

	if len(keyCertBundle.GetCertChainPem()) == 0 {
		return
	}

	certChainExpiry, err := keyCertBundle.ExtractCACertExpiryTimestamp()
	if err != nil {
		serverCaLog.Errorf("failed to extract CA cert expiry timestamp (error %v)", err)
	}
	certChainExpiryTimestamp.Record(certChainExpiry)
}

// Register registers a GRPC server on the specified port.
func (s *Server) Register(grpcServer *grpc.Server) {
	pb.RegisterIstioCertificateServiceServer(grpcServer, s)
}

// New creates a new instance of `IstioCAServiceServer`
func New(ca CertificateAuthority, ttl time.Duration,
	authenticators []security.Authenticator) (*Server, error) {
	certBundle := ca.GetCAKeyCertBundle()
	if len(certBundle.GetRootCertPem()) != 0 {
		recordCertsExpiry(certBundle)
	}
	server := &Server{
		Authenticators: authenticators,
		serverCertTTL:  ttl,
		ca:             ca,
		monitoring:     newMonitoringMetrics(),
	}
	return server, nil
}

// authenticate goes through a list of authenticators (provided client cert, k8s jwt, and ID token)
// and authenticates if one of them is valid.
func Authenticate(ctx context.Context, auth []security.Authenticator) *security.Caller {
	// TODO: apply different authenticators in specific order / according to configuration.
	var errMsg string
	for id, authn := range auth {
		u, err := authn.Authenticate(ctx)
		if err != nil {
			errMsg += fmt.Sprintf("Authenticator %s at index %d got error: %v. ", authn.AuthenticatorType(), id, err)
		}
		if u != nil && err == nil {
			serverCaLog.Debugf("Authentication successful through auth source %v", u.AuthSource)
			return u
		}
	}
	serverCaLog.Warnf("Authentication failed for %v: %s", getConnectionAddress(ctx), errMsg)
	return nil
}
<commit_msg>no need to append rootcertBytes if it is empty (#36464)<commit_after>// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ca

import (
	"fmt"
	"time"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/peer"
	"google.golang.org/grpc/status"

	pb "istio.io/api/security/v1alpha1"
	"istio.io/istio/pkg/security"
	"istio.io/istio/security/pkg/pki/ca"
	caerror "istio.io/istio/security/pkg/pki/error"
	"istio.io/istio/security/pkg/pki/util"
	"istio.io/pkg/log"
)

var serverCaLog = log.RegisterScope("serverca", "Citadel server log", 0)

// CertificateAuthority contains methods to be supported by a CA.
type CertificateAuthority interface {
	// Sign generates a certificate for a workload or CA, from the given CSR and cert opts.
	Sign(csrPEM []byte, opts ca.CertOpts) ([]byte, error)
	// SignWithCertChain is similar to Sign but returns the leaf cert and the entire cert chain.
	SignWithCertChain(csrPEM []byte, opts ca.CertOpts) ([]string, error)
	// SignWithCertChain is similar to Sign but returns the leaf cert and the entire cert chain.
	GetCAKeyCertBundle() *util.KeyCertBundle
}

// Server implements IstioCAService and IstioCertificateService and provides the services on the
// specified port.
type Server struct {
	monitoring     monitoringMetrics
	Authenticators []security.Authenticator
	ca             CertificateAuthority
	serverCertTTL  time.Duration
}

func getConnectionAddress(ctx context.Context) string {
	peerInfo, ok := peer.FromContext(ctx)
	peerAddr := "unknown"
	if ok {
		peerAddr = peerInfo.Addr.String()
	}
	return peerAddr
}

// CreateCertificate handles an incoming certificate signing request (CSR). It does
// authentication and authorization. Upon validated, signs a certificate that:
// the SAN is the identity of the caller in authentication result.
// the subject public key is the public key in the CSR.
// the validity duration is the ValidityDuration in request, or default value if the given duration is invalid.
// it is signed by the CA signing key.
func (s *Server) CreateCertificate(ctx context.Context, request *pb.IstioCertificateRequest) (
	*pb.IstioCertificateResponse, error) {
	s.monitoring.CSR.Increment()
	caller := Authenticate(ctx, s.Authenticators)
	if caller == nil {
		s.monitoring.AuthnError.Increment()
		return nil, status.Error(codes.Unauthenticated, "request authenticate failure")
	}
	// TODO: Call authorizer.
	crMetadata := request.Metadata.GetFields()
	certSigner := crMetadata[security.CertSigner].GetStringValue()
	log.Debugf("cert signer from workload %s", certSigner)
	_, _, certChainBytes, rootCertBytes := s.ca.GetCAKeyCertBundle().GetAll()
	certOpts := ca.CertOpts{
		SubjectIDs: caller.Identities,
		TTL:        time.Duration(request.ValidityDuration) * time.Second,
		ForCA:      false,
		CertSigner: certSigner,
	}
	var signErr error
	var cert []byte
	var respCertChain []string
	if certSigner == "" {
		cert, signErr = s.ca.Sign([]byte(request.Csr), certOpts)
	} else {
		respCertChain, signErr = s.ca.SignWithCertChain([]byte(request.Csr), certOpts)
	}
	if signErr != nil {
		serverCaLog.Errorf("CSR signing error (%v)", signErr.Error())
		s.monitoring.GetCertSignError(signErr.(*caerror.Error).ErrorType()).Increment()
		return nil, status.Errorf(signErr.(*caerror.Error).HTTPErrorCode(), "CSR signing error (%v)", signErr.(*caerror.Error))
	}
	if certSigner == "" {
		respCertChain = []string{string(cert)}
		if len(certChainBytes) != 0 {
			respCertChain = append(respCertChain, string(certChainBytes))
		}
	}
	if len(rootCertBytes) != 0 {
		respCertChain = append(respCertChain, string(rootCertBytes))
	}
	response := &pb.IstioCertificateResponse{
		CertChain: respCertChain,
	}
	s.monitoring.Success.Increment()
	serverCaLog.Debug("CSR successfully signed.")
	return response, nil
}

func recordCertsExpiry(keyCertBundle *util.KeyCertBundle) {
	rootCertExpiry, err := keyCertBundle.ExtractRootCertExpiryTimestamp()
	if err != nil {
		serverCaLog.Errorf("failed to extract root cert expiry timestamp (error %v)", err)
	}
	rootCertExpiryTimestamp.Record(rootCertExpiry)

	if len(keyCertBundle.GetCertChainPem()) == 0 {
		return
	}

	certChainExpiry, err := keyCertBundle.ExtractCACertExpiryTimestamp()
	if err != nil {
		serverCaLog.Errorf("failed to extract CA cert expiry timestamp (error %v)", err)
	}
	certChainExpiryTimestamp.Record(certChainExpiry)
}

// Register registers a GRPC server on the specified port.
func (s *Server) Register(grpcServer *grpc.Server) {
	pb.RegisterIstioCertificateServiceServer(grpcServer, s)
}

// New creates a new instance of `IstioCAServiceServer`
func New(ca CertificateAuthority, ttl time.Duration,
	authenticators []security.Authenticator) (*Server, error) {
	certBundle := ca.GetCAKeyCertBundle()
	if len(certBundle.GetRootCertPem()) != 0 {
		recordCertsExpiry(certBundle)
	}
	server := &Server{
		Authenticators: authenticators,
		serverCertTTL:  ttl,
		ca:             ca,
		monitoring:     newMonitoringMetrics(),
	}
	return server, nil
}

// authenticate goes through a list of authenticators (provided client cert, k8s jwt, and ID token)
// and authenticates if one of them is valid.
func Authenticate(ctx context.Context, auth []security.Authenticator) *security.Caller {
	// TODO: apply different authenticators in specific order / according to configuration.
	var errMsg string
	for id, authn := range auth {
		u, err := authn.Authenticate(ctx)
		if err != nil {
			errMsg += fmt.Sprintf("Authenticator %s at index %d got error: %v. ", authn.AuthenticatorType(), id, err)
		}
		if u != nil && err == nil {
			serverCaLog.Debugf("Authentication successful through auth source %v", u.AuthSource)
			return u
		}
	}
	serverCaLog.Warnf("Authentication failed for %v: %s", getConnectionAddress(ctx), errMsg)
	return nil
}
<|endoftext|>


<commit_before>package device

import (
	"fmt"
	"math"
	"sync"
)

// registry is the internal lookup map for devices.  it is bounded by an optional maximum number
// of connected devices.
type registry struct {
	lock    sync.RWMutex
	limit   uint32
	devices map[ID]*device
}

func newRegistry(initialCapacity, maxDevices uint32) *registry {
	if maxDevices == 0 {
		maxDevices = math.MaxUint32
	}

	return &registry{
		devices: make(map[ID]*device, initialCapacity),
		limit:   maxDevices,
	}
}

func (r *registry) maxDevices() uint32 {
	return r.limit
}

func (r *registry) add(d *device) (existing *device, deviceCount int, err error) {
	r.lock.Lock()
	existing = r.devices[d.id]

	// if there is an existing device, it will be replaced so the count won't go up
	// if there is NOT an existing device, the count will go up by one and that must be within the limit
	if existing != nil || uint32(len(r.devices)+1) <= r.limit {
		r.devices[d.id] = d
	} else {
		err = fmt.Errorf("Maximum count of devices exceeded [%d]", r.limit)
	}

	deviceCount = len(r.devices)
	r.lock.Unlock()
	return
}

func (r *registry) remove(d *device) int {
	r.lock.Lock()
	delete(r.devices, d.id)
	deviceCount := len(r.devices)
	r.lock.Unlock()

	return deviceCount
}

func (r *registry) removeID(id ID) (*device, bool) {
	r.lock.Lock()
	existing, ok := r.devices[id]
	delete(r.devices, id)
	r.lock.Unlock()

	return existing, ok
}

func (r *registry) removeIf(filter func(ID) bool, visitor func(*device)) int {
	defer r.lock.Unlock()
	r.lock.Lock()

	count := 0
	for id, candidate := range r.devices {
		if filter(id) {
			count++
			delete(r.devices, id)
			visitor(candidate)
		}
	}

	return count
}

func (r *registry) visitAll(visitor func(*device)) int {
	defer r.lock.RUnlock()
	r.lock.RLock()

	for _, d := range r.devices {
		visitor(d)
	}

	return len(r.devices)
}

func (r *registry) visitIf(filter func(ID) bool, visitor func(*device)) int {
	defer r.lock.RUnlock()
	r.lock.RLock()

	count := 0
	for id, candidate := range r.devices {
		if filter(id) {
			count++
			visitor(candidate)
		}
	}

	return count
}

func (r *registry) get(id ID) (*device, bool) {
	r.lock.RLock()
	existing, ok := r.devices[id]
	r.lock.RUnlock()

	return existing, ok
}
<commit_msg>move the fmt statement outside the mutex<commit_after>package device

import (
	"fmt"
	"math"
	"sync"
)

// registry is the internal lookup map for devices.  it is bounded by an optional maximum number
// of connected devices.
type registry struct {
	lock    sync.RWMutex
	limit   uint32
	devices map[ID]*device
}

func newRegistry(initialCapacity, maxDevices uint32) *registry {
	if maxDevices == 0 {
		maxDevices = math.MaxUint32
	}

	return &registry{
		devices: make(map[ID]*device, initialCapacity),
		limit:   maxDevices,
	}
}

func (r *registry) maxDevices() uint32 {
	return r.limit
}

func (r *registry) add(d *device) (*device, int, error) {
	r.lock.Lock()

	var (
		existing = r.devices[d.id]
		failed   = false
		err      error
	)

	// if there is an existing device, it will be replaced so the count won't go up
	// if there is NOT an existing device, the count will go up by one and that must be within the limit
	if existing != nil || uint32(len(r.devices)+1) <= r.limit {
		r.devices[d.id] = d
	} else {
		failed = true
	}

	deviceCount := len(r.devices)
	r.lock.Unlock()

	if failed {
		err = fmt.Errorf("Maximum count of devices exceeded [%d]", r.limit)
	}

	return existing, deviceCount, err
}

func (r *registry) remove(d *device) int {
	r.lock.Lock()
	delete(r.devices, d.id)
	deviceCount := len(r.devices)
	r.lock.Unlock()

	return deviceCount
}

func (r *registry) removeID(id ID) (*device, bool) {
	r.lock.Lock()
	existing, ok := r.devices[id]
	delete(r.devices, id)
	r.lock.Unlock()

	return existing, ok
}

func (r *registry) removeIf(filter func(ID) bool, visitor func(*device)) int {
	defer r.lock.Unlock()
	r.lock.Lock()

	count := 0
	for id, candidate := range r.devices {
		if filter(id) {
			count++
			delete(r.devices, id)
			visitor(candidate)
		}
	}

	return count
}

func (r *registry) visitAll(visitor func(*device)) int {
	defer r.lock.RUnlock()
	r.lock.RLock()

	for _, d := range r.devices {
		visitor(d)
	}

	return len(r.devices)
}

func (r *registry) visitIf(filter func(ID) bool, visitor func(*device)) int {
	defer r.lock.RUnlock()
	r.lock.RLock()

	count := 0
	for id, candidate := range r.devices {
		if filter(id) {
			count++
			visitor(candidate)
		}
	}

	return count
}

func (r *registry) get(id ID) (*device, bool) {
	r.lock.RLock()
	existing, ok := r.devices[id]
	r.lock.RUnlock()

	return existing, ok
}
<|endoftext|>