commit 9d801001f22b461f227e8130e7fea5c954ed865e
Author: Yongbin Kim <iam@yongbin.kim>
Date: Mon, 12 Sep 2022 15:29:12 +0900
migrated
Signed-off-by: Yongbin Kim <iam@yongbin.kim>
Diffstat:
7 files changed, 410 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,216 @@
+# Created by https://www.toptal.com/developers/gitignore/api/linux,macos,windows,goland,visualstudiocode
+# Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,windows,goland,visualstudiocode
+
+### GoLand ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### GoLand Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/sonarlint/
+
+# SonarQube Plugin
+# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
+.idea/**/sonarIssues.xml
+
+# Markdown Navigator plugin
+# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
+.idea/**/markdown-navigator.xml
+.idea/**/markdown-navigator-enh.xml
+.idea/**/markdown-navigator/
+
+# Cache file creation bug
+# See https://youtrack.jetbrains.com/issue/JBR-2257
+.idea/$CACHE_FILE$
+
+# CodeStream plugin
+# https://plugins.jetbrains.com/plugin/12206-codestream
+.idea/codestream.xml
+
+# Azure Toolkit for IntelliJ plugin
+# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
+.idea/**/azureSettings.xml
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+# Support for Project snippet scope
+.vscode/*.code-snippets
+
+# Ignore code-workspaces
+*.code-workspace
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/linux,macos,windows,goland,visualstudiocode
+n
+\ No newline at end of file
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,25 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <https://unlicense.org>
+
diff --git a/generator.go b/generator.go
@@ -0,0 +1,53 @@
+package snowflakes
+
+import (
+ "sync"
+ "time"
+)
+
+type Generator struct {
+ sync.Mutex
+
+ NodeID uint64
+ Epoch int64
+
+ lastGen int64
+ sequence uint64
+}
+
+func New(id uint64) *Generator {
+ return &Generator{
+ NodeID: id,
+ }
+}
+
+func NewWithEpoch(id uint64, epoch time.Time) *Generator {
+ return &Generator{
+ NodeID: id,
+ Epoch: epoch.UnixMilli(),
+ }
+}
+
+func (g *Generator) Next() uint64 {
+ g.Lock()
+ defer g.Unlock()
+
+ now := time.Now().UnixMilli() - g.Epoch
+ if now < 0 {
+ panic("system time can not be older than epoch")
+ }
+
+ // "now" can be past, but we won't wait.
+
+ if g.lastGen < now {
+ g.lastGen = now
+ g.sequence = 0
+ } else if g.sequence == 0x0FFF {
+ g.lastGen++
+ g.sequence = 0
+ } else {
+ g.sequence++
+ }
+
+ return uint64(g.lastGen)&0x1FFFFFFFFFF<<22 + g.NodeID&0x03FF<<12 + g.sequence&0x0FFF
+}
diff --git a/generator_test.go b/generator_test.go
@@ -0,0 +1,44 @@
+package snowflakes
+
+import (
+ "testing"
+ "time"
+)
+
+const nodeID = 0x3ff
+
+func TestGenerator_Generate(t *testing.T) {
+ var (
+ now = time.Now()
+ gen = NewWithEpoch(nodeID, now)
+ id uint64
+ )
+
+ if gen.Epoch != now.UnixMilli() {
+ t.Fatalf("NewWithEpoch failed: got %d, want %d", gen.Epoch, now.UnixMilli())
+ }
+
+ t.Run("NodeID", func(t *testing.T) {
+ id = gen.Next()
+ if (id>>12)&0x3ff != nodeID {
+ t.Fatalf(
+ "invalid node id: got: %d, want: %d",
+ (id>>12)&0x3ff, nodeID,
+ )
+ }
+ })
+
+ t.Run("id must be different from the previous", func(t *testing.T) {
+ for j := 0; j < 10000; j++ {
+ a := gen.Next()
+ b := gen.Next()
+ if a == b {
+ t.Fatalf(
+ "two is same; a is %d, b is %d. (%dth loop)",
+ a, b, j,
+ )
+ break
+ }
+ }
+ })
+}
diff --git a/go.mod b/go.mod
@@ -0,0 +1,3 @@
+module go.lair.cx/snowflake
+
+go 1.18
diff --git a/internal/base64/base64.go b/internal/base64/base64.go
@@ -0,0 +1,54 @@
+package base64
+
+import "encoding/base64"
+
+var (
+ encoding = base64.RawURLEncoding
+ withPadding = base64.URLEncoding
+)
+
+func Encode(dst, src []byte) {
+ encoding.Encode(dst, src)
+}
+
+func EncodeWithPadding(dst, src []byte) {
+ withPadding.Encode(dst, src)
+}
+
+func EncodeToString(src []byte) string {
+ return encoding.EncodeToString(src)
+}
+
+func EncodeToStringWithPadding(src []byte) string {
+ return withPadding.EncodeToString(src)
+}
+
+func EncodedLen(n int) int {
+ return encoding.EncodedLen(n)
+}
+
+func EncodedLenWithPadding(n int) int {
+ return withPadding.EncodedLen(n)
+}
+
+func Decode(dst, src []byte) (int, error) {
+ if src[len(src)-1] == '=' {
+ return withPadding.Decode(dst, src)
+ }
+ return encoding.Decode(dst, src)
+}
+
+func DecodeString(s string) ([]byte, error) {
+ if s[len(s)-1] == '=' {
+ return withPadding.DecodeString(s)
+ }
+ return encoding.DecodeString(s)
+}
+
+func DecodedLen(n int) int {
+ return encoding.DecodedLen(n)
+}
+
+func DecodedLenWithPadding(n int) int {
+ return withPadding.DecodedLen(n)
+}
diff --git a/string.go b/string.go
@@ -0,0 +1,14 @@
+package snowflakes
+
+import (
+ "encoding/binary"
+
+ "go.lair.cx/snowflake/internal/base64"
+)
+
+func String(id uint64) string {
+ buf := make([]byte, 8)
+ binary.BigEndian.PutUint64(buf, id)
+
+ return base64.EncodeToString(buf)
+}