From b5dcf0d30d04158340d642d256f241d9ecb2d1d4 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 12 Jan 2025 15:06:47 +0900 Subject: [PATCH 1/3] move kv into cloudflare/kv package --- cloudflare/kv.go | 222 ++++--------------------------------------- cloudflare/kv/kv.go | 225 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 206 deletions(-) create mode 100644 cloudflare/kv/kv.go diff --git a/cloudflare/kv.go b/cloudflare/kv.go index ead0517..23a09e6 100644 --- a/cloudflare/kv.go +++ b/cloudflare/kv.go @@ -1,225 +1,35 @@ package cloudflare import ( - "fmt" - "io" - "syscall/js" - - "github.com/syumai/workers/cloudflare/internal/cfruntimecontext" - "github.com/syumai/workers/internal/jsutil" + "github.com/syumai/workers/cloudflare/kv" ) // KVNamespace represents interface of Cloudflare Worker's KV namespace instance. -// - https://developers.cloudflare.com/workers/runtime-apis/kv/ -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L850 -type KVNamespace struct { - instance js.Value -} +// Deprecated: Use kv.Namespace instead. +type KVNamespace = kv.Namespace // NewKVNamespace returns KVNamespace for given variable name. -// - variable name must be defined in wrangler.toml as kv_namespace's binding. -// - if the given variable name doesn't exist on runtime context, returns error. -// - This function panics when a runtime context is not found. -func NewKVNamespace(varName string) (*KVNamespace, error) { - inst := cfruntimecontext.MustGetRuntimeContextEnv().Get(varName) - if inst.IsUndefined() { - return nil, fmt.Errorf("%s is undefined", varName) - } - return &KVNamespace{instance: inst}, nil +// Deprecated: Use kv.NewNamespace instead. +func NewKVNamespace(varName string) (*kv.Namespace, error) { + return kv.NewNamespace(varName) } // KVNamespaceGetOptions represents Cloudflare KV namespace get options. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L930 -type KVNamespaceGetOptions struct { - CacheTTL int -} - -func (opts *KVNamespaceGetOptions) toJS(type_ string) js.Value { - obj := jsutil.NewObject() - obj.Set("type", type_) - if opts == nil { - return obj - } - if opts.CacheTTL != 0 { - obj.Set("cacheTtl", opts.CacheTTL) - } - return obj -} - -// GetString gets string value by the specified key. -// - if a network error happens, returns error. -func (kv *KVNamespace) GetString(key string, opts *KVNamespaceGetOptions) (string, error) { - p := kv.instance.Call("get", key, opts.toJS("text")) - v, err := jsutil.AwaitPromise(p) - if err != nil { - return "", err - } - return v.String(), nil -} - -// GetReader gets stream value by the specified key. -// - if a network error happens, returns error. -func (kv *KVNamespace) GetReader(key string, opts *KVNamespaceGetOptions) (io.Reader, error) { - p := kv.instance.Call("get", key, opts.toJS("stream")) - v, err := jsutil.AwaitPromise(p) - if err != nil { - return nil, err - } - return jsutil.ConvertReadableStreamToReadCloser(v), nil -} +// Deprecated: Use kv.GetOptions instead. +type KVNamespaceGetOptions = kv.GetOptions // KVNamespaceListOptions represents Cloudflare KV namespace list options. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L946 -type KVNamespaceListOptions struct { - Limit int - Prefix string - Cursor string -} - -func (opts *KVNamespaceListOptions) toJS() js.Value { - if opts == nil { - return js.Undefined() - } - obj := jsutil.NewObject() - if opts.Limit != 0 { - obj.Set("limit", opts.Limit) - } - if opts.Prefix != "" { - obj.Set("prefix", opts.Prefix) - } - if opts.Cursor != "" { - obj.Set("cursor", opts.Cursor) - } - return obj -} +// Deprecated: Use kv.ListOptions instead. +type KVNamespaceListOptions = kv.ListOptions // KVNamespaceListKey represents Cloudflare KV namespace list key. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L940 -type KVNamespaceListKey struct { - Name string - // Expiration is an expiration of KV value cache. The value `0` means no expiration. - Expiration int - // Metadata map[string]any // TODO: implement -} - -// toKVNamespaceListResult converts JavaScript side's KVNamespaceListKey to *KVNamespaceListKey. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L940 -func toKVNamespaceListKey(v js.Value) (*KVNamespaceListKey, error) { - expVal := v.Get("expiration") - var exp int - if !expVal.IsUndefined() { - exp = expVal.Int() - } - return &KVNamespaceListKey{ - Name: v.Get("name").String(), - Expiration: exp, - // Metadata // TODO: implement. This may return an error, so this func signature has an error in return parameters. - }, nil -} +// Deprecated: Use kv.ListKey instead. +type KVNamespaceListKey = kv.ListKey // KVNamespaceListResult represents Cloudflare KV namespace list result. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L952 -type KVNamespaceListResult struct { - Keys []*KVNamespaceListKey - ListComplete bool - Cursor string -} - -// toKVNamespaceListResult converts JavaScript side's KVNamespaceListResult to *KVNamespaceListResult. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L952 -func toKVNamespaceListResult(v js.Value) (*KVNamespaceListResult, error) { - keysVal := v.Get("keys") - keys := make([]*KVNamespaceListKey, keysVal.Length()) - for i := 0; i < len(keys); i++ { - key, err := toKVNamespaceListKey(keysVal.Index(i)) - if err != nil { - return nil, fmt.Errorf("error converting to KVNamespaceListKey: %w", err) - } - keys[i] = key - } - - cursorVal := v.Get("cursor") - var cursor string - if !cursorVal.IsUndefined() { - cursor = cursorVal.String() - } - - return &KVNamespaceListResult{ - Keys: keys, - ListComplete: v.Get("list_complete").Bool(), - Cursor: cursor, - }, nil -} - -// List lists keys stored into the KV namespace. -func (kv *KVNamespace) List(opts *KVNamespaceListOptions) (*KVNamespaceListResult, error) { - p := kv.instance.Call("list", opts.toJS()) - v, err := jsutil.AwaitPromise(p) - if err != nil { - return nil, err - } - return toKVNamespaceListResult(v) -} +// Deprecated: Use kv.ListResult instead. +type KVNamespaceListResult = kv.ListResult // KVNamespacePutOptions represents Cloudflare KV namespace put options. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L958 -type KVNamespacePutOptions struct { - Expiration int - ExpirationTTL int - // Metadata // TODO: implement -} - -func (opts *KVNamespacePutOptions) toJS() js.Value { - if opts == nil { - return js.Undefined() - } - obj := jsutil.NewObject() - if opts.Expiration != 0 { - obj.Set("expiration", opts.Expiration) - } - if opts.ExpirationTTL != 0 { - obj.Set("expirationTtl", opts.ExpirationTTL) - } - return obj -} - -// PutString puts string value into KV with key. -// - if a network error happens, returns error. -func (kv *KVNamespace) PutString(key string, value string, opts *KVNamespacePutOptions) error { - p := kv.instance.Call("put", key, value, opts.toJS()) - _, err := jsutil.AwaitPromise(p) - if err != nil { - return err - } - return nil -} - -// PutReader puts stream value into KV with key. -// - This method copies all bytes into memory for implementation restriction. -// - if a network error happens, returns error. -func (kv *KVNamespace) PutReader(key string, value io.Reader, opts *KVNamespacePutOptions) error { - // fetch body cannot be ReadableStream. see: https://github.com/whatwg/fetch/issues/1438 - b, err := io.ReadAll(value) - if err != nil { - return err - } - ua := jsutil.NewUint8Array(len(b)) - js.CopyBytesToJS(ua, b) - p := kv.instance.Call("put", key, ua.Get("buffer"), opts.toJS()) - _, err = jsutil.AwaitPromise(p) - if err != nil { - return err - } - return nil -} - -// Delete deletes key-value pair specified by the key. -// - if a network error happens, returns error. -func (kv *KVNamespace) Delete(key string) error { - p := kv.instance.Call("delete", key) - _, err := jsutil.AwaitPromise(p) - if err != nil { - return err - } - return nil -} +// Deprecated: Use kv.PutOptions instead. +type KVNamespacePutOptions = kv.PutOptions diff --git a/cloudflare/kv/kv.go b/cloudflare/kv/kv.go new file mode 100644 index 0000000..9662cda --- /dev/null +++ b/cloudflare/kv/kv.go @@ -0,0 +1,225 @@ +package kv + +import ( + "fmt" + "io" + "syscall/js" + + "github.com/syumai/workers/cloudflare/internal/cfruntimecontext" + "github.com/syumai/workers/internal/jsutil" +) + +// Namespace represents interface of Cloudflare Worker's KV namespace instance. +// - https://developers.cloudflare.com/workers/runtime-apis/kv/ +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L850 +type Namespace struct { + instance js.Value +} + +// NewNamespace returns Namespace for given variable name. +// - variable name must be defined in wrangler.toml as kv_namespace's binding. +// - if the given variable name doesn't exist on runtime context, returns error. +// - This function panics when a runtime context is not found. +func NewNamespace(varName string) (*Namespace, error) { + inst := cfruntimecontext.MustGetRuntimeContextEnv().Get(varName) + if inst.IsUndefined() { + return nil, fmt.Errorf("%s is undefined", varName) + } + return &Namespace{instance: inst}, nil +} + +// GetOptions represents Cloudflare KV namespace get options. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L930 +type GetOptions struct { + CacheTTL int +} + +func (opts *GetOptions) toJS(type_ string) js.Value { + obj := jsutil.NewObject() + obj.Set("type", type_) + if opts == nil { + return obj + } + if opts.CacheTTL != 0 { + obj.Set("cacheTtl", opts.CacheTTL) + } + return obj +} + +// GetString gets string value by the specified key. +// - if a network error happens, returns error. +func (ns *Namespace) GetString(key string, opts *GetOptions) (string, error) { + p := ns.instance.Call("get", key, opts.toJS("text")) + v, err := jsutil.AwaitPromise(p) + if err != nil { + return "", err + } + return v.String(), nil +} + +// GetReader gets stream value by the specified key. +// - if a network error happens, returns error. +func (ns *Namespace) GetReader(key string, opts *GetOptions) (io.Reader, error) { + p := ns.instance.Call("get", key, opts.toJS("stream")) + v, err := jsutil.AwaitPromise(p) + if err != nil { + return nil, err + } + return jsutil.ConvertReadableStreamToReadCloser(v), nil +} + +// ListOptions represents Cloudflare KV namespace list options. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L946 +type ListOptions struct { + Limit int + Prefix string + Cursor string +} + +func (opts *ListOptions) toJS() js.Value { + if opts == nil { + return js.Undefined() + } + obj := jsutil.NewObject() + if opts.Limit != 0 { + obj.Set("limit", opts.Limit) + } + if opts.Prefix != "" { + obj.Set("prefix", opts.Prefix) + } + if opts.Cursor != "" { + obj.Set("cursor", opts.Cursor) + } + return obj +} + +// ListKey represents Cloudflare KV namespace list key. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L940 +type ListKey struct { + Name string + // Expiration is an expiration of KV value cache. The value `0` means no expiration. + Expiration int + // Metadata map[string]any // TODO: implement +} + +// toListKey converts JavaScript side's KVNamespaceListKey to *ListKey. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L940 +func toListKey(v js.Value) (*ListKey, error) { + expVal := v.Get("expiration") + var exp int + if !expVal.IsUndefined() { + exp = expVal.Int() + } + return &ListKey{ + Name: v.Get("name").String(), + Expiration: exp, + // Metadata // TODO: implement. This may return an error, so this func signature has an error in return parameters. + }, nil +} + +// ListResult represents Cloudflare KV namespace list result. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L952 +type ListResult struct { + Keys []*ListKey + ListComplete bool + Cursor string +} + +// toListResult converts JavaScript side's KVNamespaceListResult to *ListResult. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L952 +func toListResult(v js.Value) (*ListResult, error) { + keysVal := v.Get("keys") + keys := make([]*ListKey, keysVal.Length()) + for i := 0; i < len(keys); i++ { + key, err := toListKey(keysVal.Index(i)) + if err != nil { + return nil, fmt.Errorf("error converting to ListKey: %w", err) + } + keys[i] = key + } + + cursorVal := v.Get("cursor") + var cursor string + if !cursorVal.IsUndefined() { + cursor = cursorVal.String() + } + + return &ListResult{ + Keys: keys, + ListComplete: v.Get("list_complete").Bool(), + Cursor: cursor, + }, nil +} + +// List lists keys stored into the KV namespace. +func (ns *Namespace) List(opts *ListOptions) (*ListResult, error) { + p := ns.instance.Call("list", opts.toJS()) + v, err := jsutil.AwaitPromise(p) + if err != nil { + return nil, err + } + return toListResult(v) +} + +// PutOptions represents Cloudflare KV namespace put options. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L958 +type PutOptions struct { + Expiration int + ExpirationTTL int + // Metadata // TODO: implement +} + +func (opts *PutOptions) toJS() js.Value { + if opts == nil { + return js.Undefined() + } + obj := jsutil.NewObject() + if opts.Expiration != 0 { + obj.Set("expiration", opts.Expiration) + } + if opts.ExpirationTTL != 0 { + obj.Set("expirationTtl", opts.ExpirationTTL) + } + return obj +} + +// PutString puts string value into KV with key. +// - if a network error happens, returns error. +func (ns *Namespace) PutString(key string, value string, opts *PutOptions) error { + p := ns.instance.Call("put", key, value, opts.toJS()) + _, err := jsutil.AwaitPromise(p) + if err != nil { + return err + } + return nil +} + +// PutReader puts stream value into KV with key. +// - This method copies all bytes into memory for implementation restriction. +// - if a network error happens, returns error. +func (ns *Namespace) PutReader(key string, value io.Reader, opts *PutOptions) error { + // fetch body cannot be ReadableStream. see: https://github.com/whatwg/fetch/issues/1438 + b, err := io.ReadAll(value) + if err != nil { + return err + } + ua := jsutil.NewUint8Array(len(b)) + js.CopyBytesToJS(ua, b) + p := ns.instance.Call("put", key, ua.Get("buffer"), opts.toJS()) + _, err = jsutil.AwaitPromise(p) + if err != nil { + return err + } + return nil +} + +// Delete deletes key-value pair specified by the key. +// - if a network error happens, returns error. +func (ns *Namespace) Delete(key string) error { + p := ns.instance.Call("delete", key) + _, err := jsutil.AwaitPromise(p) + if err != nil { + return err + } + return nil +} From 43b36c424871adaf02c354db7341ad3c9f63a08f Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 12 Jan 2025 21:43:20 +0900 Subject: [PATCH 2/3] update KV example --- _examples/kv-counter/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_examples/kv-counter/main.go b/_examples/kv-counter/main.go index 643926a..97f681d 100644 --- a/_examples/kv-counter/main.go +++ b/_examples/kv-counter/main.go @@ -8,7 +8,7 @@ import ( "strconv" "github.com/syumai/workers" - "github.com/syumai/workers/cloudflare" + "github.com/syumai/workers/cloudflare/kv" ) // counterNamespace is a bounded KV namespace for storing counter. @@ -31,13 +31,13 @@ func main() { } // initialize KV namespace instance - kv, err := cloudflare.NewKVNamespace(counterNamespace) + counterKV, err := kv.NewNamespace(counterNamespace) if err != nil { fmt.Fprintf(os.Stderr, "failed to init KV: %v", err) os.Exit(1) } - countStr, err := kv.GetString(countKey, nil) + countStr, err := counterKV.GetString(countKey, nil) if err != nil { handleErr(w, "failed to get current count\n", err) return @@ -48,7 +48,7 @@ func main() { nextCountStr := strconv.Itoa(count + 1) - err = kv.PutString(countKey, nextCountStr, nil) + err = counterKV.PutString(countKey, nextCountStr, nil) if err != nil { handleErr(w, "failed to put next count\n", err) return From c5873af47680fbb3054be8c74cefa544d493d7c3 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 12 Jan 2025 22:01:02 +0900 Subject: [PATCH 3/3] split kv.go --- cloudflare/kv/delete.go | 16 +++ cloudflare/kv/get.go | 48 ++++++++ cloudflare/kv/kv.go | 225 ------------------------------------- cloudflare/kv/list.go | 101 +++++++++++++++++ cloudflare/kv/namespace.go | 27 +++++ cloudflare/kv/put.go | 60 ++++++++++ 6 files changed, 252 insertions(+), 225 deletions(-) create mode 100644 cloudflare/kv/delete.go create mode 100644 cloudflare/kv/get.go delete mode 100644 cloudflare/kv/kv.go create mode 100644 cloudflare/kv/list.go create mode 100644 cloudflare/kv/namespace.go create mode 100644 cloudflare/kv/put.go diff --git a/cloudflare/kv/delete.go b/cloudflare/kv/delete.go new file mode 100644 index 0000000..3dac26e --- /dev/null +++ b/cloudflare/kv/delete.go @@ -0,0 +1,16 @@ +package kv + +import ( + "github.com/syumai/workers/internal/jsutil" +) + +// Delete deletes key-value pair specified by the key. +// - if a network error happens, returns error. +func (ns *Namespace) Delete(key string) error { + p := ns.instance.Call("delete", key) + _, err := jsutil.AwaitPromise(p) + if err != nil { + return err + } + return nil +} diff --git a/cloudflare/kv/get.go b/cloudflare/kv/get.go new file mode 100644 index 0000000..b83f3a3 --- /dev/null +++ b/cloudflare/kv/get.go @@ -0,0 +1,48 @@ +package kv + +import ( + "io" + "syscall/js" + + "github.com/syumai/workers/internal/jsutil" +) + +// GetOptions represents Cloudflare KV namespace get options. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L930 +type GetOptions struct { + CacheTTL int +} + +func (opts *GetOptions) toJS(type_ string) js.Value { + obj := jsutil.NewObject() + obj.Set("type", type_) + if opts == nil { + return obj + } + if opts.CacheTTL != 0 { + obj.Set("cacheTtl", opts.CacheTTL) + } + return obj +} + +// GetString gets string value by the specified key. +// - if a network error happens, returns error. +func (ns *Namespace) GetString(key string, opts *GetOptions) (string, error) { + p := ns.instance.Call("get", key, opts.toJS("text")) + v, err := jsutil.AwaitPromise(p) + if err != nil { + return "", err + } + return v.String(), nil +} + +// GetReader gets stream value by the specified key. +// - if a network error happens, returns error. +func (ns *Namespace) GetReader(key string, opts *GetOptions) (io.Reader, error) { + p := ns.instance.Call("get", key, opts.toJS("stream")) + v, err := jsutil.AwaitPromise(p) + if err != nil { + return nil, err + } + return jsutil.ConvertReadableStreamToReadCloser(v), nil +} diff --git a/cloudflare/kv/kv.go b/cloudflare/kv/kv.go deleted file mode 100644 index 9662cda..0000000 --- a/cloudflare/kv/kv.go +++ /dev/null @@ -1,225 +0,0 @@ -package kv - -import ( - "fmt" - "io" - "syscall/js" - - "github.com/syumai/workers/cloudflare/internal/cfruntimecontext" - "github.com/syumai/workers/internal/jsutil" -) - -// Namespace represents interface of Cloudflare Worker's KV namespace instance. -// - https://developers.cloudflare.com/workers/runtime-apis/kv/ -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L850 -type Namespace struct { - instance js.Value -} - -// NewNamespace returns Namespace for given variable name. -// - variable name must be defined in wrangler.toml as kv_namespace's binding. -// - if the given variable name doesn't exist on runtime context, returns error. -// - This function panics when a runtime context is not found. -func NewNamespace(varName string) (*Namespace, error) { - inst := cfruntimecontext.MustGetRuntimeContextEnv().Get(varName) - if inst.IsUndefined() { - return nil, fmt.Errorf("%s is undefined", varName) - } - return &Namespace{instance: inst}, nil -} - -// GetOptions represents Cloudflare KV namespace get options. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L930 -type GetOptions struct { - CacheTTL int -} - -func (opts *GetOptions) toJS(type_ string) js.Value { - obj := jsutil.NewObject() - obj.Set("type", type_) - if opts == nil { - return obj - } - if opts.CacheTTL != 0 { - obj.Set("cacheTtl", opts.CacheTTL) - } - return obj -} - -// GetString gets string value by the specified key. -// - if a network error happens, returns error. -func (ns *Namespace) GetString(key string, opts *GetOptions) (string, error) { - p := ns.instance.Call("get", key, opts.toJS("text")) - v, err := jsutil.AwaitPromise(p) - if err != nil { - return "", err - } - return v.String(), nil -} - -// GetReader gets stream value by the specified key. -// - if a network error happens, returns error. -func (ns *Namespace) GetReader(key string, opts *GetOptions) (io.Reader, error) { - p := ns.instance.Call("get", key, opts.toJS("stream")) - v, err := jsutil.AwaitPromise(p) - if err != nil { - return nil, err - } - return jsutil.ConvertReadableStreamToReadCloser(v), nil -} - -// ListOptions represents Cloudflare KV namespace list options. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L946 -type ListOptions struct { - Limit int - Prefix string - Cursor string -} - -func (opts *ListOptions) toJS() js.Value { - if opts == nil { - return js.Undefined() - } - obj := jsutil.NewObject() - if opts.Limit != 0 { - obj.Set("limit", opts.Limit) - } - if opts.Prefix != "" { - obj.Set("prefix", opts.Prefix) - } - if opts.Cursor != "" { - obj.Set("cursor", opts.Cursor) - } - return obj -} - -// ListKey represents Cloudflare KV namespace list key. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L940 -type ListKey struct { - Name string - // Expiration is an expiration of KV value cache. The value `0` means no expiration. - Expiration int - // Metadata map[string]any // TODO: implement -} - -// toListKey converts JavaScript side's KVNamespaceListKey to *ListKey. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L940 -func toListKey(v js.Value) (*ListKey, error) { - expVal := v.Get("expiration") - var exp int - if !expVal.IsUndefined() { - exp = expVal.Int() - } - return &ListKey{ - Name: v.Get("name").String(), - Expiration: exp, - // Metadata // TODO: implement. This may return an error, so this func signature has an error in return parameters. - }, nil -} - -// ListResult represents Cloudflare KV namespace list result. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L952 -type ListResult struct { - Keys []*ListKey - ListComplete bool - Cursor string -} - -// toListResult converts JavaScript side's KVNamespaceListResult to *ListResult. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L952 -func toListResult(v js.Value) (*ListResult, error) { - keysVal := v.Get("keys") - keys := make([]*ListKey, keysVal.Length()) - for i := 0; i < len(keys); i++ { - key, err := toListKey(keysVal.Index(i)) - if err != nil { - return nil, fmt.Errorf("error converting to ListKey: %w", err) - } - keys[i] = key - } - - cursorVal := v.Get("cursor") - var cursor string - if !cursorVal.IsUndefined() { - cursor = cursorVal.String() - } - - return &ListResult{ - Keys: keys, - ListComplete: v.Get("list_complete").Bool(), - Cursor: cursor, - }, nil -} - -// List lists keys stored into the KV namespace. -func (ns *Namespace) List(opts *ListOptions) (*ListResult, error) { - p := ns.instance.Call("list", opts.toJS()) - v, err := jsutil.AwaitPromise(p) - if err != nil { - return nil, err - } - return toListResult(v) -} - -// PutOptions represents Cloudflare KV namespace put options. -// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L958 -type PutOptions struct { - Expiration int - ExpirationTTL int - // Metadata // TODO: implement -} - -func (opts *PutOptions) toJS() js.Value { - if opts == nil { - return js.Undefined() - } - obj := jsutil.NewObject() - if opts.Expiration != 0 { - obj.Set("expiration", opts.Expiration) - } - if opts.ExpirationTTL != 0 { - obj.Set("expirationTtl", opts.ExpirationTTL) - } - return obj -} - -// PutString puts string value into KV with key. -// - if a network error happens, returns error. -func (ns *Namespace) PutString(key string, value string, opts *PutOptions) error { - p := ns.instance.Call("put", key, value, opts.toJS()) - _, err := jsutil.AwaitPromise(p) - if err != nil { - return err - } - return nil -} - -// PutReader puts stream value into KV with key. -// - This method copies all bytes into memory for implementation restriction. -// - if a network error happens, returns error. -func (ns *Namespace) PutReader(key string, value io.Reader, opts *PutOptions) error { - // fetch body cannot be ReadableStream. see: https://github.com/whatwg/fetch/issues/1438 - b, err := io.ReadAll(value) - if err != nil { - return err - } - ua := jsutil.NewUint8Array(len(b)) - js.CopyBytesToJS(ua, b) - p := ns.instance.Call("put", key, ua.Get("buffer"), opts.toJS()) - _, err = jsutil.AwaitPromise(p) - if err != nil { - return err - } - return nil -} - -// Delete deletes key-value pair specified by the key. -// - if a network error happens, returns error. -func (ns *Namespace) Delete(key string) error { - p := ns.instance.Call("delete", key) - _, err := jsutil.AwaitPromise(p) - if err != nil { - return err - } - return nil -} diff --git a/cloudflare/kv/list.go b/cloudflare/kv/list.go new file mode 100644 index 0000000..2fc0bcb --- /dev/null +++ b/cloudflare/kv/list.go @@ -0,0 +1,101 @@ +package kv + +import ( + "fmt" + "syscall/js" + + "github.com/syumai/workers/internal/jsutil" +) + +// ListOptions represents Cloudflare KV namespace list options. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L946 +type ListOptions struct { + Limit int + Prefix string + Cursor string +} + +func (opts *ListOptions) toJS() js.Value { + if opts == nil { + return js.Undefined() + } + obj := jsutil.NewObject() + if opts.Limit != 0 { + obj.Set("limit", opts.Limit) + } + if opts.Prefix != "" { + obj.Set("prefix", opts.Prefix) + } + if opts.Cursor != "" { + obj.Set("cursor", opts.Cursor) + } + return obj +} + +// ListKey represents Cloudflare KV namespace list key. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L940 +type ListKey struct { + Name string + // Expiration is an expiration of KV value cache. The value `0` means no expiration. + Expiration int + // Metadata map[string]any // TODO: implement +} + +// toListKey converts JavaScript side's KVNamespaceListKey to *ListKey. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L940 +func toListKey(v js.Value) (*ListKey, error) { + expVal := v.Get("expiration") + var exp int + if !expVal.IsUndefined() { + exp = expVal.Int() + } + return &ListKey{ + Name: v.Get("name").String(), + Expiration: exp, + // Metadata // TODO: implement. This may return an error, so this func signature has an error in return parameters. + }, nil +} + +// ListResult represents Cloudflare KV namespace list result. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L952 +type ListResult struct { + Keys []*ListKey + ListComplete bool + Cursor string +} + +// toListResult converts JavaScript side's KVNamespaceListResult to *ListResult. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L952 +func toListResult(v js.Value) (*ListResult, error) { + keysVal := v.Get("keys") + keys := make([]*ListKey, keysVal.Length()) + for i := 0; i < len(keys); i++ { + key, err := toListKey(keysVal.Index(i)) + if err != nil { + return nil, fmt.Errorf("error converting to ListKey: %w", err) + } + keys[i] = key + } + + cursorVal := v.Get("cursor") + var cursor string + if !cursorVal.IsUndefined() { + cursor = cursorVal.String() + } + + return &ListResult{ + Keys: keys, + ListComplete: v.Get("list_complete").Bool(), + Cursor: cursor, + }, nil +} + +// List lists keys stored into the KV namespace. +func (ns *Namespace) List(opts *ListOptions) (*ListResult, error) { + p := ns.instance.Call("list", opts.toJS()) + v, err := jsutil.AwaitPromise(p) + if err != nil { + return nil, err + } + return toListResult(v) +} diff --git a/cloudflare/kv/namespace.go b/cloudflare/kv/namespace.go new file mode 100644 index 0000000..f873fe5 --- /dev/null +++ b/cloudflare/kv/namespace.go @@ -0,0 +1,27 @@ +package kv + +import ( + "fmt" + "syscall/js" + + "github.com/syumai/workers/cloudflare/internal/cfruntimecontext" +) + +// Namespace represents interface of Cloudflare Worker's KV namespace instance. +// - https://developers.cloudflare.com/workers/runtime-apis/kv/ +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L850 +type Namespace struct { + instance js.Value +} + +// NewNamespace returns Namespace for given variable name. +// - variable name must be defined in wrangler.toml as kv_namespace's binding. +// - if the given variable name doesn't exist on runtime context, returns error. +// - This function panics when a runtime context is not found. +func NewNamespace(varName string) (*Namespace, error) { + inst := cfruntimecontext.MustGetRuntimeContextEnv().Get(varName) + if inst.IsUndefined() { + return nil, fmt.Errorf("%s is undefined", varName) + } + return &Namespace{instance: inst}, nil +} diff --git a/cloudflare/kv/put.go b/cloudflare/kv/put.go new file mode 100644 index 0000000..c101965 --- /dev/null +++ b/cloudflare/kv/put.go @@ -0,0 +1,60 @@ +package kv + +import ( + "io" + "syscall/js" + + "github.com/syumai/workers/internal/jsutil" +) + +// PutOptions represents Cloudflare KV namespace put options. +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L958 +type PutOptions struct { + Expiration int + ExpirationTTL int + // Metadata // TODO: implement +} + +func (opts *PutOptions) toJS() js.Value { + if opts == nil { + return js.Undefined() + } + obj := jsutil.NewObject() + if opts.Expiration != 0 { + obj.Set("expiration", opts.Expiration) + } + if opts.ExpirationTTL != 0 { + obj.Set("expirationTtl", opts.ExpirationTTL) + } + return obj +} + +// PutString puts string value into KV with key. +// - if a network error happens, returns error. +func (ns *Namespace) PutString(key string, value string, opts *PutOptions) error { + p := ns.instance.Call("put", key, value, opts.toJS()) + _, err := jsutil.AwaitPromise(p) + if err != nil { + return err + } + return nil +} + +// PutReader puts stream value into KV with key. +// - This method copies all bytes into memory for implementation restriction. +// - if a network error happens, returns error. +func (ns *Namespace) PutReader(key string, value io.Reader, opts *PutOptions) error { + // fetch body cannot be ReadableStream. see: https://github.com/whatwg/fetch/issues/1438 + b, err := io.ReadAll(value) + if err != nil { + return err + } + ua := jsutil.NewUint8Array(len(b)) + js.CopyBytesToJS(ua, b) + p := ns.instance.Call("put", key, ua.Get("buffer"), opts.toJS()) + _, err = jsutil.AwaitPromise(p) + if err != nil { + return err + } + return nil +}