From d5e1ad9087aecd6b67369b9ebbeb633ad808c129 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 21 Jun 2025 06:14:20 +0000
Subject: [PATCH 1/4] feat(client): add escape hatch for null slice & maps
---
internal/encoding/json/encode.go | 13 +--
internal/encoding/json/sentinel/null.go | 61 ++++++-------
.../encoding/json/sentinel/sentinel_test.go | 85 ++++++++++---------
internal/paramutil/sentinel.go | 31 -------
packages/param/null.go | 19 +++++
packages/param/null_test.go | 49 +++++++++++
packages/param/param.go | 16 +++-
7 files changed, 153 insertions(+), 121 deletions(-)
delete mode 100644 internal/paramutil/sentinel.go
create mode 100644 packages/param/null.go
create mode 100644 packages/param/null_test.go
diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go
index e80e536..053c709 100644
--- a/internal/encoding/json/encode.go
+++ b/internal/encoding/json/encode.go
@@ -776,7 +776,7 @@ type mapEncoder struct {
}
func (me mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
- if v.IsNil() {
+ if v.IsNil() /* EDIT(begin) */ || sentinel.IsValueNull(v) /* EDIT(end) */ {
e.WriteString("null")
return
}
@@ -855,7 +855,7 @@ type sliceEncoder struct {
}
func (se sliceEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
- if v.IsNil() {
+ if v.IsNil() /* EDIT(begin) */ || sentinel.IsValueNull(v) /* EDIT(end) */ {
e.WriteString("null")
return
}
@@ -916,14 +916,7 @@ type ptrEncoder struct {
}
func (pe ptrEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
- // EDIT(begin)
- //
- // if v.IsNil() {
- // e.WriteString("null")
- // return
- // }
-
- if v.IsNil() || sentinel.IsValueNullPtr(v) || sentinel.IsValueNullSlice(v) {
+ if v.IsNil() {
e.WriteString("null")
return
}
diff --git a/internal/encoding/json/sentinel/null.go b/internal/encoding/json/sentinel/null.go
index a7ed3b7..32216c4 100644
--- a/internal/encoding/json/sentinel/null.go
+++ b/internal/encoding/json/sentinel/null.go
@@ -6,52 +6,41 @@ import (
"sync"
)
-var nullPtrsCache sync.Map // map[reflect.Type]*T
-
-func NullPtr[T any]() *T {
- t := shims.TypeFor[T]()
- ptr, loaded := nullPtrsCache.Load(t) // avoid premature allocation
- if !loaded {
- ptr, _ = nullPtrsCache.LoadOrStore(t, new(T))
- }
- return (ptr.(*T))
+type cacheEntry struct {
+ x any
+ ptr uintptr
+ kind reflect.Kind
}
-var nullSlicesCache sync.Map // map[reflect.Type][]T
+var nullCache sync.Map // map[reflect.Type]cacheEntry
-func NullSlice[T any]() []T {
+func NewNullSentinel[T any](mk func() T) T {
t := shims.TypeFor[T]()
- slice, loaded := nullSlicesCache.Load(t) // avoid premature allocation
+ entry, loaded := nullCache.Load(t) // avoid premature allocation
if !loaded {
- slice, _ = nullSlicesCache.LoadOrStore(t, []T{})
+ x := mk()
+ ptr := reflect.ValueOf(x).Pointer()
+ entry, _ = nullCache.LoadOrStore(t, cacheEntry{x, ptr, t.Kind()})
}
- return slice.([]T)
+ return entry.(cacheEntry).x.(T)
}
-func IsNullPtr[T any](ptr *T) bool {
- nullptr, ok := nullPtrsCache.Load(shims.TypeFor[T]())
- return ok && ptr == nullptr.(*T)
-}
-
-func IsNullSlice[T any](slice []T) bool {
- nullSlice, ok := nullSlicesCache.Load(shims.TypeFor[T]())
- return ok && reflect.ValueOf(slice).Pointer() == reflect.ValueOf(nullSlice).Pointer()
-}
-
-// internal only
-func IsValueNullPtr(v reflect.Value) bool {
- if v.Kind() != reflect.Ptr {
- return false
+// for internal use only
+func IsValueNull(v reflect.Value) bool {
+ switch v.Kind() {
+ case reflect.Map, reflect.Slice:
+ null, ok := nullCache.Load(v.Type())
+ return ok && v.Pointer() == null.(cacheEntry).ptr
}
- nullptr, ok := nullPtrsCache.Load(v.Type().Elem())
- return ok && v.Pointer() == reflect.ValueOf(nullptr).Pointer()
+ return false
}
-// internal only
-func IsValueNullSlice(v reflect.Value) bool {
- if v.Kind() != reflect.Slice {
- return false
+func IsNull[T any](v T) bool {
+ t := shims.TypeFor[T]()
+ switch t.Kind() {
+ case reflect.Map, reflect.Slice:
+ null, ok := nullCache.Load(t)
+ return ok && reflect.ValueOf(v).Pointer() == null.(cacheEntry).ptr
}
- nullSlice, ok := nullSlicesCache.Load(v.Type().Elem())
- return ok && v.Pointer() == reflect.ValueOf(nullSlice).Pointer()
+ return false
}
diff --git a/internal/encoding/json/sentinel/sentinel_test.go b/internal/encoding/json/sentinel/sentinel_test.go
index 4ecaa8e..623d50e 100644
--- a/internal/encoding/json/sentinel/sentinel_test.go
+++ b/internal/encoding/json/sentinel/sentinel_test.go
@@ -2,6 +2,7 @@ package sentinel_test
import (
"github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel"
+ "github.com/onkernel/kernel-go-sdk/packages/param"
"reflect"
"slices"
"testing"
@@ -15,25 +16,19 @@ type Pair struct {
func TestNullSlice(t *testing.T) {
var nilSlice []int = nil
var nonNilSlice []int = []int{1, 2, 3}
- var nullSlice []int = sentinel.NullSlice[int]()
+ var nullSlice []int = param.NullSlice[[]int]()
cases := map[string]Pair{
- "nilSlice": {sentinel.IsNullSlice(nilSlice), false},
- "nullSlice": {sentinel.IsNullSlice(nullSlice), true},
- "newNullSlice": {sentinel.IsNullSlice(sentinel.NullSlice[int]()), true},
+ "nilSlice": {sentinel.IsNull(nilSlice), false},
+ "nullSlice": {sentinel.IsNull(nullSlice), true},
+ "newNullSlice": {sentinel.IsNull(param.NullSlice[[]int]()), true},
"lenNullSlice": {len(nullSlice) == 0, true},
- "nilSliceValue": {sentinel.IsValueNullSlice(reflect.ValueOf(nilSlice)), false},
- "nullSliceValue": {sentinel.IsValueNullSlice(reflect.ValueOf(nullSlice)), true},
+ "nilSliceValue": {sentinel.IsValueNull(reflect.ValueOf(nilSlice)), false},
+ "nullSliceValue": {sentinel.IsValueNull(reflect.ValueOf(nullSlice)), true},
"compareSlices": {slices.Compare(nilSlice, nullSlice) == 0, true},
"compareNonNilSlices": {slices.Compare(nonNilSlice, nullSlice) == 0, false},
}
- nilSlice = append(nullSlice, 12)
- cases["append_result"] = Pair{sentinel.IsNullSlice(nilSlice), false}
- cases["mutated_result"] = Pair{sentinel.IsNullSlice(nullSlice), true}
- cases["append_result_len"] = Pair{len(nilSlice) == 1, true}
- cases["append_null_slice_len"] = Pair{len(nullSlice) == 0, true}
-
for name, c := range cases {
t.Run(name, func(t *testing.T) {
got, want := c.got, c.want
@@ -44,37 +39,20 @@ func TestNullSlice(t *testing.T) {
}
}
-func TestNullPtr(t *testing.T) {
- var s *string = nil
- var i *int = nil
- var slice *[]int = nil
-
- var nullptrStr *string = sentinel.NullPtr[string]()
- var nullptrInt *int = sentinel.NullPtr[int]()
- var nullptrSlice *[]int = sentinel.NullPtr[[]int]()
-
- if *nullptrStr != "" {
- t.Errorf("Failed to safely deref")
- }
- if *nullptrInt != 0 {
- t.Errorf("Failed to safely deref")
- }
- if len(*nullptrSlice) != 0 {
- t.Errorf("Failed to safely deref")
- }
+func TestNullMap(t *testing.T) {
+ var nilMap map[string]int = nil
+ var nonNilMap map[string]int = map[string]int{"a": 1, "b": 2}
+ var nullMap map[string]int = param.NullMap[map[string]int]()
cases := map[string]Pair{
- "nilStr": {sentinel.IsNullPtr(s), false},
- "nullStr": {sentinel.IsNullPtr(nullptrStr), true},
-
- "nilInt": {sentinel.IsNullPtr(i), false},
- "nullInt": {sentinel.IsNullPtr(nullptrInt), true},
-
- "nilSlice": {sentinel.IsNullPtr(slice), false},
- "nullSlice": {sentinel.IsNullPtr(nullptrSlice), true},
-
- "nilValuePtr": {sentinel.IsValueNullPtr(reflect.ValueOf(i)), false},
- "nullValuePtr": {sentinel.IsValueNullPtr(reflect.ValueOf(nullptrInt)), true},
+ "nilMap": {sentinel.IsNull(nilMap), false},
+ "nullMap": {sentinel.IsNull(nullMap), true},
+ "newNullMap": {sentinel.IsNull(param.NullMap[map[string]int]()), true},
+ "lenNullMap": {len(nullMap) == 0, true},
+ "nilMapValue": {sentinel.IsValueNull(reflect.ValueOf(nilMap)), false},
+ "nullMapValue": {sentinel.IsValueNull(reflect.ValueOf(nullMap)), true},
+ "compareMaps": {reflect.DeepEqual(nilMap, nullMap), false},
+ "compareNonNilMaps": {reflect.DeepEqual(nonNilMap, nullMap), false},
}
for name, test := range cases {
@@ -86,3 +64,28 @@ func TestNullPtr(t *testing.T) {
})
}
}
+
+func TestIsNullRepeated(t *testing.T) {
+ // Test for slices
+ nullSlice1 := param.NullSlice[[]int]()
+ nullSlice2 := param.NullSlice[[]int]()
+ if !sentinel.IsNull(nullSlice1) {
+ t.Errorf("IsNull(nullSlice1) = false, want true")
+ }
+ if !sentinel.IsNull(nullSlice2) {
+ t.Errorf("IsNull(nullSlice2) = false, want true")
+ }
+ if !sentinel.IsNull(nullSlice1) || !sentinel.IsNull(nullSlice2) {
+ t.Errorf("IsNull should return true for all NullSlice instances")
+ }
+
+ // Test for maps
+ nullMap1 := param.NullMap[map[string]int]()
+ nullMap2 := param.NullMap[map[string]int]()
+ if !sentinel.IsNull(nullMap1) {
+ t.Errorf("IsNull(nullMap1) = false, want true")
+ }
+ if !sentinel.IsNull(nullMap2) {
+ t.Errorf("IsNull(nullMap2) = false, want true")
+ }
+}
diff --git a/internal/paramutil/sentinel.go b/internal/paramutil/sentinel.go
deleted file mode 100644
index a2aed25..0000000
--- a/internal/paramutil/sentinel.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package paramutil
-
-import (
- "github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel"
-)
-
-// NullPtr returns a pointer to the zero value of the type T.
-// When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null.
-//
-// It is unspecified behavior to mutate the value pointed to by the returned pointer.
-func NullPtr[T any]() *T {
- return sentinel.NullPtr[T]()
-}
-
-// IsNullPtr returns true if the pointer was created by [NullPtr].
-func IsNullPtr[T any](ptr *T) bool {
- return sentinel.IsNullPtr(ptr)
-}
-
-// NullSlice returns a non-nil slice with a length of 0.
-// When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null.
-//
-// It is undefined behavior to mutate the slice returned by [NullSlice].
-func NullSlice[T any]() []T {
- return sentinel.NullSlice[T]()
-}
-
-// IsNullSlice returns true if the slice was created by [NullSlice].
-func IsNullSlice[T any](slice []T) bool {
- return sentinel.IsNullSlice(slice)
-}
diff --git a/packages/param/null.go b/packages/param/null.go
new file mode 100644
index 0000000..7a4afe9
--- /dev/null
+++ b/packages/param/null.go
@@ -0,0 +1,19 @@
+package param
+
+import "github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel"
+
+// NullMap returns a non-nil map with a length of 0.
+// When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null.
+//
+// It is unspecified behavior to mutate the slice returned by [NullSlice].
+func NullMap[MapT ~map[string]T, T any]() MapT {
+ return sentinel.NewNullSentinel(func() MapT { return make(MapT, 1) })
+}
+
+// NullSlice returns a non-nil slice with a length of 0.
+// When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null.
+//
+// It is unspecified behavior to mutate the slice returned by [NullSlice].
+func NullSlice[SliceT ~[]T, T any]() SliceT {
+ return sentinel.NewNullSentinel(func() SliceT { return make(SliceT, 0, 1) })
+}
diff --git a/packages/param/null_test.go b/packages/param/null_test.go
new file mode 100644
index 0000000..0ecd68e
--- /dev/null
+++ b/packages/param/null_test.go
@@ -0,0 +1,49 @@
+package param_test
+
+import (
+ "encoding/json"
+ "github.com/onkernel/kernel-go-sdk/packages/param"
+ "testing"
+)
+
+type Nullables struct {
+ Slice []int `json:"slice,omitzero"`
+ Map map[string]int `json:"map,omitzero"`
+ param.APIObject
+}
+
+func (n Nullables) MarshalJSON() ([]byte, error) {
+ type shadow Nullables
+ return param.MarshalObject(n, (*shadow)(&n))
+}
+
+func TestNullMarshal(t *testing.T) {
+ bytes, err := json.Marshal(Nullables{})
+ if err != nil {
+ t.Fatalf("json error %v", err.Error())
+ }
+ if string(bytes) != `{}` {
+ t.Fatalf("expected empty object, got %s", string(bytes))
+ }
+
+ obj := Nullables{
+ Slice: param.NullSlice[[]int](),
+ Map: param.NullMap[map[string]int](),
+ }
+ bytes, err = json.Marshal(obj)
+
+ if !param.IsNull(obj.Slice) {
+ t.Fatal("failed null check")
+ }
+ if !param.IsNull(obj.Map) {
+ t.Fatal("failed null check")
+ }
+
+ if err != nil {
+ t.Fatalf("json error %v", err.Error())
+ }
+ exp := `{"slice":null,"map":null}`
+ if string(bytes) != exp {
+ t.Fatalf("expected %s, got %s", exp, string(bytes))
+ }
+}
diff --git a/packages/param/param.go b/packages/param/param.go
index c1bc099..2ad956f 100644
--- a/packages/param/param.go
+++ b/packages/param/param.go
@@ -2,6 +2,7 @@ package param
import (
"encoding/json"
+ "github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel"
"reflect"
)
@@ -58,12 +59,21 @@ func IsOmitted(v any) bool {
// IsNull returns true if v was set to the JSON value null.
//
-// To set a param to null use [NullStruct] or [Null]
+// To set a param to null use [NullStruct], [Null], [NullMap], or [NullSlice]
// depending on the type of v.
//
// IsNull returns false if the value is omitted.
-func IsNull(v ParamNullable) bool {
- return v.null()
+func IsNull[T any](v T) bool {
+ if nullable, ok := any(v).(ParamNullable); ok {
+ return nullable.null()
+ }
+
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Slice, reflect.Map:
+ return sentinel.IsNull(v)
+ }
+
+ return false
}
// ParamNullable encapsulates all structs in parameters,
From a62b9647386501f43e70ad876dc5c0271c4c4709 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 21 Jun 2025 06:55:43 +0000
Subject: [PATCH 2/4] chore: fix documentation of null map
---
packages/param/null.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/param/null.go b/packages/param/null.go
index 7a4afe9..9281a4b 100644
--- a/packages/param/null.go
+++ b/packages/param/null.go
@@ -5,7 +5,7 @@ import "github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel"
// NullMap returns a non-nil map with a length of 0.
// When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null.
//
-// It is unspecified behavior to mutate the slice returned by [NullSlice].
+// It is unspecified behavior to mutate the map returned by [NullMap].
func NullMap[MapT ~map[string]T, T any]() MapT {
return sentinel.NewNullSentinel(func() MapT { return make(MapT, 1) })
}
From dc72c81a2e45918d595c6c00843b1a1d0efffdd0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Jun 2025 19:49:35 +0000
Subject: [PATCH 3/4] feat(api): add `since` parameter to deployment logs
endpoint
---
.stats.yml | 6 +++---
aliases.go | 5 +++++
api.md | 3 ++-
app.go | 3 +++
appdeployment.go | 13 +++++++++++--
client_test.go | 7 ++++++-
deployment.go | 30 ++++++++++++++++++++++++++----
invocation.go | 13 +++++++++++--
shared/constant/constants.go | 3 +++
shared/shared.go | 24 ++++++++++++++++++++++++
10 files changed, 94 insertions(+), 13 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 4a84456..b296d07 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 16
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b019e469425a59061f37c5fdc7a131a5291c66134ef0627db4f06bb1f4af0b15.yml
-openapi_spec_hash: f66a3c2efddb168db9539ba2507b10b8
-config_hash: aae6721b2be9ec8565dfc8f7eadfe105
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2aec229ccf91f7c1ac95aa675ea2a59bd61af9e363a22c3b49677992f1eeb16a.yml
+openapi_spec_hash: c80cd5d52a79cd5366a76d4a825bd27a
+config_hash: b8e1fff080fbaa22656ab0a57b591777
diff --git a/aliases.go b/aliases.go
index 3295d1b..e0267f2 100644
--- a/aliases.go
+++ b/aliases.go
@@ -27,6 +27,11 @@ type ErrorEvent = shared.ErrorEvent
// This is an alias to an internal type.
type ErrorModel = shared.ErrorModel
+// Heartbeat event sent periodically to keep SSE connection alive.
+//
+// This is an alias to an internal type.
+type HeartbeatEvent = shared.HeartbeatEvent
+
// A log entry from the application.
//
// This is an alias to an internal type.
diff --git a/api.md b/api.md
index e1bf732..2a53bb8 100644
--- a/api.md
+++ b/api.md
@@ -3,6 +3,7 @@
- shared.ErrorDetail
- shared.ErrorEvent
- shared.ErrorModel
+- shared.HeartbeatEvent
- shared.LogEvent
# Deployments
@@ -18,7 +19,7 @@ Methods:
- client.Deployments.New(ctx context.Context, body kernel.DeploymentNewParams) (kernel.DeploymentNewResponse, error)
- client.Deployments.Get(ctx context.Context, id string) (kernel.DeploymentGetResponse, error)
-- client.Deployments.Follow(ctx context.Context, id string) (kernel.DeploymentFollowResponseUnion, error)
+- client.Deployments.Follow(ctx context.Context, id string, query kernel.DeploymentFollowParams) (kernel.DeploymentFollowResponseUnion, error)
# Apps
diff --git a/app.go b/app.go
index a38ef15..b469553 100644
--- a/app.go
+++ b/app.go
@@ -51,6 +51,8 @@ type AppListResponse struct {
ID string `json:"id,required"`
// Name of the application
AppName string `json:"app_name,required"`
+ // Deployment ID
+ Deployment string `json:"deployment,required"`
// Deployment region code
Region constant.AwsUsEast1a `json:"region,required"`
// Version label for the application
@@ -61,6 +63,7 @@ type AppListResponse struct {
JSON struct {
ID respjson.Field
AppName respjson.Field
+ Deployment respjson.Field
Region respjson.Field
Version respjson.Field
EnvVars respjson.Field
diff --git a/appdeployment.go b/appdeployment.go
index 0199bd0..692bc62 100644
--- a/appdeployment.go
+++ b/appdeployment.go
@@ -147,14 +147,15 @@ const (
// AppDeploymentFollowResponseUnion contains all possible properties and values
// from [AppDeploymentFollowResponseState],
-// [AppDeploymentFollowResponseStateUpdate], [shared.LogEvent].
+// [AppDeploymentFollowResponseStateUpdate], [shared.LogEvent],
+// [shared.HeartbeatEvent].
//
// Use the [AppDeploymentFollowResponseUnion.AsAny] method to switch on the
// variant.
//
// Use the methods beginning with 'As' to cast the union to one of its variants.
type AppDeploymentFollowResponseUnion struct {
- // Any of "state", "state_update", "log".
+ // Any of "state", "state_update", "log", "sse_heartbeat".
Event string `json:"event"`
State string `json:"state"`
Timestamp time.Time `json:"timestamp"`
@@ -185,6 +186,7 @@ func (AppDeploymentFollowResponseStateUpdate) ImplAppDeploymentFollowResponseUni
// case kernel.AppDeploymentFollowResponseState:
// case kernel.AppDeploymentFollowResponseStateUpdate:
// case shared.LogEvent:
+// case shared.HeartbeatEvent:
// default:
// fmt.Errorf("no variant present")
// }
@@ -196,6 +198,8 @@ func (u AppDeploymentFollowResponseUnion) AsAny() anyAppDeploymentFollowResponse
return u.AsStateUpdate()
case "log":
return u.AsLog()
+ case "sse_heartbeat":
+ return u.AsSseHeartbeat()
}
return nil
}
@@ -215,6 +219,11 @@ func (u AppDeploymentFollowResponseUnion) AsLog() (v shared.LogEvent) {
return
}
+func (u AppDeploymentFollowResponseUnion) AsSseHeartbeat() (v shared.HeartbeatEvent) {
+ apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v)
+ return
+}
+
// Returns the unmodified JSON received from the API
func (u AppDeploymentFollowResponseUnion) RawJSON() string { return u.JSON.raw }
diff --git a/client_test.go b/client_test.go
index 6abf48c..96b8cb9 100644
--- a/client_test.go
+++ b/client_test.go
@@ -311,7 +311,11 @@ func TestContextDeadlineStreaming(t *testing.T) {
},
}),
)
- stream := client.Deployments.FollowStreaming(deadlineCtx, "id")
+ stream := client.Deployments.FollowStreaming(
+ deadlineCtx,
+ "id",
+ kernel.DeploymentFollowParams{},
+ )
for stream.Next() {
_ = stream.Current()
}
@@ -359,6 +363,7 @@ func TestContextDeadlineStreamingWithRequestTimeout(t *testing.T) {
stream := client.Deployments.FollowStreaming(
context.Background(),
"id",
+ kernel.DeploymentFollowParams{},
option.WithRequestTimeout((100 * time.Millisecond)),
)
for stream.Next() {
diff --git a/deployment.go b/deployment.go
index 70422aa..26f1d23 100644
--- a/deployment.go
+++ b/deployment.go
@@ -11,10 +11,12 @@ import (
"io"
"mime/multipart"
"net/http"
+ "net/url"
"time"
"github.com/onkernel/kernel-go-sdk/internal/apiform"
"github.com/onkernel/kernel-go-sdk/internal/apijson"
+ "github.com/onkernel/kernel-go-sdk/internal/apiquery"
"github.com/onkernel/kernel-go-sdk/internal/requestconfig"
"github.com/onkernel/kernel-go-sdk/option"
"github.com/onkernel/kernel-go-sdk/packages/param"
@@ -66,7 +68,7 @@ func (r *DeploymentService) Get(ctx context.Context, id string, opts ...option.R
// Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and
// status updates for a deployment. The stream terminates automatically once the
// deployment reaches a terminal state.
-func (r *DeploymentService) FollowStreaming(ctx context.Context, id string, opts ...option.RequestOption) (stream *ssestream.Stream[DeploymentFollowResponseUnion]) {
+func (r *DeploymentService) FollowStreaming(ctx context.Context, id string, query DeploymentFollowParams, opts ...option.RequestOption) (stream *ssestream.Stream[DeploymentFollowResponseUnion]) {
var (
raw *http.Response
err error
@@ -78,7 +80,7 @@ func (r *DeploymentService) FollowStreaming(ctx context.Context, id string, opts
return
}
path := fmt.Sprintf("deployments/%s/events", id)
- err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &raw, opts...)
return ssestream.NewStream[DeploymentFollowResponseUnion](ssestream.NewDecoder(raw), err)
}
@@ -253,13 +255,14 @@ const (
// DeploymentFollowResponseUnion contains all possible properties and values from
// [shared.LogEvent], [DeploymentStateEvent],
-// [DeploymentFollowResponseAppVersionSummaryEvent], [shared.ErrorEvent].
+// [DeploymentFollowResponseAppVersionSummaryEvent], [shared.ErrorEvent],
+// [shared.HeartbeatEvent].
//
// Use the [DeploymentFollowResponseUnion.AsAny] method to switch on the variant.
//
// Use the methods beginning with 'As' to cast the union to one of its variants.
type DeploymentFollowResponseUnion struct {
- // Any of "log", "deployment_state", nil, nil.
+ // Any of "log", "deployment_state", nil, nil, "sse_heartbeat".
Event string `json:"event"`
// This field is from variant [shared.LogEvent].
Message string `json:"message"`
@@ -316,6 +319,11 @@ func (u DeploymentFollowResponseUnion) AsErrorEvent() (v shared.ErrorEvent) {
return
}
+func (u DeploymentFollowResponseUnion) AsSseHeartbeat() (v shared.HeartbeatEvent) {
+ apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v)
+ return
+}
+
// Returns the unmodified JSON received from the API
func (u DeploymentFollowResponseUnion) RawJSON() string { return u.JSON.raw }
@@ -423,3 +431,17 @@ type DeploymentNewParamsRegion string
const (
DeploymentNewParamsRegionAwsUsEast1a DeploymentNewParamsRegion = "aws.us-east-1a"
)
+
+type DeploymentFollowParams struct {
+ // Show logs since the given time (RFC timestamps or durations like 5m).
+ Since param.Opt[string] `query:"since,omitzero" json:"-"`
+ paramObj
+}
+
+// URLQuery serializes [DeploymentFollowParams]'s query parameters as `url.Values`.
+func (r DeploymentFollowParams) URLQuery() (v url.Values, err error) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
+ ArrayFormat: apiquery.ArrayQueryFormatComma,
+ NestedFormat: apiquery.NestedQueryFormatBrackets,
+ })
+}
diff --git a/invocation.go b/invocation.go
index f74b814..5dc72ae 100644
--- a/invocation.go
+++ b/invocation.go
@@ -321,13 +321,14 @@ const (
)
// InvocationFollowResponseUnion contains all possible properties and values from
-// [shared.LogEvent], [InvocationStateEvent], [shared.ErrorEvent].
+// [shared.LogEvent], [InvocationStateEvent], [shared.ErrorEvent],
+// [shared.HeartbeatEvent].
//
// Use the [InvocationFollowResponseUnion.AsAny] method to switch on the variant.
//
// Use the methods beginning with 'As' to cast the union to one of its variants.
type InvocationFollowResponseUnion struct {
- // Any of "log", "invocation_state", "error".
+ // Any of "log", "invocation_state", "error", "sse_heartbeat".
Event string `json:"event"`
// This field is from variant [shared.LogEvent].
Message string `json:"message"`
@@ -361,6 +362,7 @@ func (InvocationStateEvent) ImplInvocationFollowResponseUnion() {}
// case shared.LogEvent:
// case kernel.InvocationStateEvent:
// case shared.ErrorEvent:
+// case shared.HeartbeatEvent:
// default:
// fmt.Errorf("no variant present")
// }
@@ -372,6 +374,8 @@ func (u InvocationFollowResponseUnion) AsAny() anyInvocationFollowResponse {
return u.AsInvocationState()
case "error":
return u.AsError()
+ case "sse_heartbeat":
+ return u.AsSseHeartbeat()
}
return nil
}
@@ -391,6 +395,11 @@ func (u InvocationFollowResponseUnion) AsError() (v shared.ErrorEvent) {
return
}
+func (u InvocationFollowResponseUnion) AsSseHeartbeat() (v shared.HeartbeatEvent) {
+ apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v)
+ return
+}
+
// Returns the unmodified JSON received from the API
func (u InvocationFollowResponseUnion) RawJSON() string { return u.JSON.raw }
diff --git a/shared/constant/constants.go b/shared/constant/constants.go
index 2b9f076..d1e41c7 100644
--- a/shared/constant/constants.go
+++ b/shared/constant/constants.go
@@ -24,6 +24,7 @@ type DeploymentState string // Always "deployment_state"
type Error string // Always "error"
type InvocationState string // Always "invocation_state"
type Log string // Always "log"
+type SseHeartbeat string // Always "sse_heartbeat"
type State string // Always "state"
type StateUpdate string // Always "state_update"
@@ -33,6 +34,7 @@ func (c DeploymentState) Default() DeploymentState { return "deployment_stat
func (c Error) Default() Error { return "error" }
func (c InvocationState) Default() InvocationState { return "invocation_state" }
func (c Log) Default() Log { return "log" }
+func (c SseHeartbeat) Default() SseHeartbeat { return "sse_heartbeat" }
func (c State) Default() State { return "state" }
func (c StateUpdate) Default() StateUpdate { return "state_update" }
@@ -42,6 +44,7 @@ func (c DeploymentState) MarshalJSON() ([]byte, error) { return marshalString(
func (c Error) MarshalJSON() ([]byte, error) { return marshalString(c) }
func (c InvocationState) MarshalJSON() ([]byte, error) { return marshalString(c) }
func (c Log) MarshalJSON() ([]byte, error) { return marshalString(c) }
+func (c SseHeartbeat) MarshalJSON() ([]byte, error) { return marshalString(c) }
func (c State) MarshalJSON() ([]byte, error) { return marshalString(c) }
func (c StateUpdate) MarshalJSON() ([]byte, error) { return marshalString(c) }
diff --git a/shared/shared.go b/shared/shared.go
index 4b63e7c..0115c3c 100644
--- a/shared/shared.go
+++ b/shared/shared.go
@@ -87,6 +87,30 @@ func (r *ErrorModel) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Heartbeat event sent periodically to keep SSE connection alive.
+type HeartbeatEvent struct {
+ // Event type identifier (always "sse_heartbeat").
+ Event constant.SseHeartbeat `json:"event,required"`
+ // Time the heartbeat was sent.
+ Timestamp time.Time `json:"timestamp,required" format:"date-time"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Event respjson.Field
+ Timestamp respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r HeartbeatEvent) RawJSON() string { return r.JSON.raw }
+func (r *HeartbeatEvent) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (HeartbeatEvent) ImplAppDeploymentFollowResponseUnion() {}
+func (HeartbeatEvent) ImplInvocationFollowResponseUnion() {}
+
// A log entry from the application.
type LogEvent struct {
// Event type identifier (always "log").
From 86cafea8ea3772f4857fa1719ed2f9e0a1999143 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Jun 2025 19:51:33 +0000
Subject: [PATCH 4/4] release: 0.6.2
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 14 ++++++++++++++
README.md | 2 +-
internal/version.go | 2 +-
4 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index ac03171..e3778b2 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.6.1"
+ ".": "0.6.2"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9bece6..d38e6f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## 0.6.2 (2025-06-24)
+
+Full Changelog: [v0.6.1...v0.6.2](https://github.com/onkernel/kernel-go-sdk/compare/v0.6.1...v0.6.2)
+
+### Features
+
+* **api:** add `since` parameter to deployment logs endpoint ([dc72c81](https://github.com/onkernel/kernel-go-sdk/commit/dc72c81a2e45918d595c6c00843b1a1d0efffdd0))
+* **client:** add escape hatch for null slice & maps ([d5e1ad9](https://github.com/onkernel/kernel-go-sdk/commit/d5e1ad9087aecd6b67369b9ebbeb633ad808c129))
+
+
+### Chores
+
+* fix documentation of null map ([a62b964](https://github.com/onkernel/kernel-go-sdk/commit/a62b9647386501f43e70ad876dc5c0271c4c4709))
+
## 0.6.1 (2025-06-18)
Full Changelog: [v0.6.0...v0.6.1](https://github.com/onkernel/kernel-go-sdk/compare/v0.6.0...v0.6.1)
diff --git a/README.md b/README.md
index bef30e9..3058e0b 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Or to pin the version:
```sh
-go get -u 'github.com/onkernel/kernel-go-sdk@v0.6.1'
+go get -u 'github.com/onkernel/kernel-go-sdk@v0.6.2'
```
diff --git a/internal/version.go b/internal/version.go
index 6dfc989..01e812b 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,4 +2,4 @@
package internal
-const PackageVersion = "0.6.1" // x-release-please-version
+const PackageVersion = "0.6.2" // x-release-please-version