Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,13 @@ type NamespaceStore interface {
}
~~~

绑定方式有三种:
绑定方式有五种:

- `BindNamespace`:将指定的 store 以 Promise API 绑定为全局对象(如 KV);
- `BindSyncNamespace`:将指定的 store 以同步 API 绑定为全局对象(如 SyncKV),
便于测试或脚本场景;
- `BindNamespaceObject`:将 Promise API 安装到已有 JS 对象,不创建全局变量;
- `BindSyncNamespaceObject`:将同步 API 安装到已有 JS 对象,不创建全局变量;
- `InstallConstructor`:安装 `KVNamespace` 构造器,由 resolver 按 namespace
名称解析对应的 store,模拟 Workers 的多命名空间绑定。

Expand All @@ -333,6 +335,23 @@ if err := cloudflarekv.BindSyncNamespace(rt, "SyncKV", kv); err != nil {
return err
}

// 安装到已有对象,适用于 storage.kv / storage.synckv 等嵌套绑定:
storage := rt.NewObject()
asyncObject := rt.NewObject()
if err := cloudflarekv.BindNamespaceObject(rt, loop, asyncObject, kv); err != nil {
return err
}
syncObject := rt.NewObject()
if err := cloudflarekv.BindSyncNamespaceObject(rt, syncObject, kv); err != nil {
return err
}
if err := storage.Set("kv", asyncObject); err != nil {
return err
}
if err := storage.Set("synckv", syncObject); err != nil {
return err
}

// 多命名空间构造器:
resolver := func(namespace string) (store.NamespaceStore, error) {
switch namespace {
Expand Down
33 changes: 33 additions & 0 deletions cloudflarekv/behavior_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,39 @@ func TestBindNamespaceValidation(t *testing.T) {
}
}

func TestBindNamespaceObjectValidation(t *testing.T) {
loop := eventloop.NewEventLoop()
loop.Start()
defer loop.Stop()

mem := newMemStore()
vm := goja.New()
target := vm.NewObject()

tests := []struct {
name string
bind func() error
want string
}{
{"async nil runtime", func() error { return cloudflarekv.BindNamespaceObject(nil, loop, target, mem) }, "runtime is required"},
{"async nil loop", func() error { return cloudflarekv.BindNamespaceObject(vm, nil, target, mem) }, "event loop is required"},
{"async nil target", func() error { return cloudflarekv.BindNamespaceObject(vm, loop, nil, mem) }, "target is required"},
{"async nil store", func() error { return cloudflarekv.BindNamespaceObject(vm, loop, target, nil) }, "store is required"},
{"sync nil runtime", func() error { return cloudflarekv.BindSyncNamespaceObject(nil, target, mem) }, "runtime is required"},
{"sync nil target", func() error { return cloudflarekv.BindSyncNamespaceObject(vm, nil, mem) }, "target is required"},
{"sync nil store", func() error { return cloudflarekv.BindSyncNamespaceObject(vm, target, nil) }, "store is required"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.bind()
if err == nil || err.Error() != test.want {
t.Fatalf("error = %v, want %q", err, test.want)
}
})
}
}

func TestPutClassifiesJSONStrings(t *testing.T) {
loop := eventloop.NewEventLoop()
loop.Start()
Expand Down
37 changes: 35 additions & 2 deletions cloudflarekv/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,30 @@ func BindNamespace(vm *goja.Runtime, loop *eventloop.EventLoop, bindingName stri
}

object := vm.NewObject()
if err := bindObject(vm, loop, object, ns); err != nil {
if err := BindNamespaceObject(vm, loop, object, ns); err != nil {
return err
}
return vm.Set(bindingName, object)
}

// BindNamespaceObject installs the asynchronous KV API on an existing object.
func BindNamespaceObject(vm *goja.Runtime, loop *eventloop.EventLoop, target *goja.Object, ns store.NamespaceStore) error {
if vm == nil {
return errors.New("runtime is required")
}
if loop == nil {
return errors.New("event loop is required")
}
if target == nil {
return errors.New("target is required")
}
if ns == nil {
return errors.New("store is required")
}

return bindObject(vm, loop, target, ns)
}

func BindSyncNamespace(vm *goja.Runtime, bindingName string, ns store.NamespaceStore) error {
if vm == nil {
return errors.New("runtime is required")
Expand All @@ -79,12 +97,27 @@ func BindSyncNamespace(vm *goja.Runtime, bindingName string, ns store.NamespaceS
}

object := vm.NewObject()
if err := bindSyncObject(vm, object, ns); err != nil {
if err := BindSyncNamespaceObject(vm, object, ns); err != nil {
return err
}
return vm.Set(bindingName, object)
}

// BindSyncNamespaceObject installs the synchronous KV API on an existing object.
func BindSyncNamespaceObject(vm *goja.Runtime, target *goja.Object, ns store.NamespaceStore) error {
if vm == nil {
return errors.New("runtime is required")
}
if target == nil {
return errors.New("target is required")
}
if ns == nil {
return errors.New("store is required")
}

return bindSyncObject(vm, target, ns)
}

func bindObject(vm *goja.Runtime, loop *eventloop.EventLoop, object *goja.Object, ns store.NamespaceStore) error {
if err := object.Set("get", func(call goja.FunctionCall) goja.Value {
key := call.Argument(0).String()
Expand Down
53 changes: 53 additions & 0 deletions cloudflarekv/bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,59 @@ func TestBindNamespaceSupportsJSONListAndMetadata(t *testing.T) {
}
}

func TestBindNamespaceObjectsSupportNestedAsyncAndSyncAccessWithoutGlobals(t *testing.T) {
loop := eventloop.NewEventLoop()
loop.Start()
defer loop.Stop()

mem := newMemStore()
result := runScript(t, loop, func(vm *goja.Runtime) error {
storage := vm.NewObject()
kv := vm.NewObject()
if err := cloudflarekv.BindNamespaceObject(vm, loop, kv, mem); err != nil {
return err
}
syncKV := vm.NewObject()
if err := cloudflarekv.BindSyncNamespaceObject(vm, syncKV, mem); err != nil {
return err
}
if err := storage.Set("kv", kv); err != nil {
return err
}
if err := storage.Set("synckv", syncKV); err != nil {
return err
}
if err := vm.Set("storage", storage); err != nil {
return err
}

_, err := vm.RunString(`
if (typeof KV !== "undefined" || typeof SyncKV !== "undefined") {
throw new Error("namespace object binding leaked a global");
}
storage.synckv.put("sync", "sync-value");
storage.kv.put("async", "async-value")
.then(function () {
return storage.kv.get("async");
})
.then(function (asyncValue) {
done(JSON.stringify({
asyncValue: asyncValue,
syncValue: storage.synckv.get("sync")
}));
})
.catch(function (err) {
fail(String(err));
});
`)
return err
})

if result != `{"asyncValue":"async-value","syncValue":"sync-value"}` {
t.Fatalf("unexpected nested storage result: %s", result)
}
}

func TestBindNamespaceSupportsTypedArrays(t *testing.T) {
loop := eventloop.NewEventLoop()
loop.Start()
Expand Down
Loading