goscript 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/cmd/goscript/cmd_compile.go +28 -8
  2. package/cmd/goscript/cmd_compile_test.go +105 -6
  3. package/compiler/build-flags.go +9 -10
  4. package/compiler/gotest/runner_test.go +127 -0
  5. package/compiler/lowering.go +596 -136
  6. package/compiler/lowering_bench_test.go +350 -0
  7. package/compiler/package-graph.go +61 -4
  8. package/compiler/package-graph_test.go +30 -0
  9. package/compiler/semantic-model-types.go +8 -0
  10. package/compiler/semantic-model.go +447 -22
  11. package/compiler/semantic-model_test.go +138 -0
  12. package/compiler/skeleton_test.go +948 -14
  13. package/compiler/typescript-emitter.go +19 -2
  14. package/dist/gs/builtin/builtin.d.ts +2 -2
  15. package/dist/gs/builtin/builtin.js +20 -0
  16. package/dist/gs/builtin/builtin.js.map +1 -1
  17. package/dist/gs/builtin/slice.js +5 -0
  18. package/dist/gs/builtin/slice.js.map +1 -1
  19. package/dist/gs/builtin/type.d.ts +1 -1
  20. package/dist/gs/builtin/type.js +72 -5
  21. package/dist/gs/builtin/type.js.map +1 -1
  22. package/dist/gs/compress/zlib/index.d.ts +3 -3
  23. package/dist/gs/compress/zlib/index.js +88 -26
  24. package/dist/gs/compress/zlib/index.js.map +1 -1
  25. package/dist/gs/crypto/sha1/index.js +2 -5
  26. package/dist/gs/crypto/sha1/index.js.map +1 -1
  27. package/dist/gs/crypto/sha256/index.js +2 -5
  28. package/dist/gs/crypto/sha256/index.js.map +1 -1
  29. package/dist/gs/crypto/sha512/index.js +2 -5
  30. package/dist/gs/crypto/sha512/index.js.map +1 -1
  31. package/dist/gs/embed/index.d.ts +6 -0
  32. package/dist/gs/embed/index.js +210 -5
  33. package/dist/gs/embed/index.js.map +1 -1
  34. package/dist/gs/fmt/fmt.d.ts +3 -3
  35. package/dist/gs/fmt/fmt.js +29 -16
  36. package/dist/gs/fmt/fmt.js.map +1 -1
  37. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +118 -6
  38. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +1 -1
  39. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.d.ts +45 -0
  40. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js +229 -0
  41. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js.map +1 -0
  42. package/dist/gs/io/fs/readdir.js +5 -3
  43. package/dist/gs/io/fs/readdir.js.map +1 -1
  44. package/dist/gs/io/io.d.ts +10 -6
  45. package/dist/gs/io/io.js +87 -42
  46. package/dist/gs/io/io.js.map +1 -1
  47. package/dist/gs/math/bits/index.d.ts +26 -5
  48. package/dist/gs/math/bits/index.js +13 -24
  49. package/dist/gs/math/bits/index.js.map +1 -1
  50. package/dist/gs/net/http/index.d.ts +3 -1
  51. package/dist/gs/net/http/index.js +18 -1
  52. package/dist/gs/net/http/index.js.map +1 -1
  53. package/dist/gs/os/types_js.gs.d.ts +6 -2
  54. package/dist/gs/os/types_js.gs.js +169 -8
  55. package/dist/gs/os/types_js.gs.js.map +1 -1
  56. package/dist/gs/reflect/type.d.ts +1 -0
  57. package/dist/gs/reflect/type.js +80 -51
  58. package/dist/gs/reflect/type.js.map +1 -1
  59. package/dist/gs/strings/reader.d.ts +1 -1
  60. package/dist/gs/strings/reader.js +2 -2
  61. package/dist/gs/strings/reader.js.map +1 -1
  62. package/dist/gs/sync/sync.d.ts +2 -1
  63. package/dist/gs/sync/sync.js +37 -16
  64. package/dist/gs/sync/sync.js.map +1 -1
  65. package/dist/gs/syscall/js/index.js +9 -0
  66. package/dist/gs/syscall/js/index.js.map +1 -1
  67. package/dist/gs/testing/testing.js +8 -6
  68. package/dist/gs/testing/testing.js.map +1 -1
  69. package/gs/builtin/builtin.ts +25 -2
  70. package/gs/builtin/runtime-contract.test.ts +45 -0
  71. package/gs/builtin/slice.ts +7 -0
  72. package/gs/builtin/type.ts +85 -5
  73. package/gs/compress/zlib/index.test.ts +97 -0
  74. package/gs/compress/zlib/index.ts +117 -27
  75. package/gs/compress/zlib/meta.json +4 -1
  76. package/gs/crypto/sha1/index.test.ts +19 -2
  77. package/gs/crypto/sha1/index.ts +3 -6
  78. package/gs/crypto/sha256/index.test.ts +14 -2
  79. package/gs/crypto/sha256/index.ts +3 -6
  80. package/gs/crypto/sha512/index.test.ts +17 -2
  81. package/gs/crypto/sha512/index.ts +3 -6
  82. package/gs/embed/index.test.ts +87 -0
  83. package/gs/embed/index.ts +229 -5
  84. package/gs/fmt/fmt.test.ts +41 -3
  85. package/gs/fmt/fmt.ts +40 -17
  86. package/gs/fmt/meta.json +6 -1
  87. package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +8 -1
  88. package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +139 -11
  89. package/gs/github.com/go-git/go-billy/v6/osfs/index.test.ts +110 -0
  90. package/gs/github.com/go-git/go-billy/v6/osfs/index.ts +280 -0
  91. package/gs/github.com/go-git/go-billy/v6/osfs/meta.json +8 -0
  92. package/gs/io/fs/readdir.test.ts +38 -0
  93. package/gs/io/fs/readdir.ts +7 -3
  94. package/gs/io/io.test.ts +77 -6
  95. package/gs/io/io.ts +114 -52
  96. package/gs/io/meta.json +7 -1
  97. package/gs/math/bits/index.ts +52 -28
  98. package/gs/net/http/index.test.ts +16 -0
  99. package/gs/net/http/index.ts +19 -2
  100. package/gs/os/file_unix_js.test.ts +52 -0
  101. package/gs/os/meta.json +4 -0
  102. package/gs/os/readdir.test.ts +56 -0
  103. package/gs/os/types_js.gs.ts +169 -8
  104. package/gs/reflect/deepequal.test.ts +10 -1
  105. package/gs/reflect/type.ts +91 -56
  106. package/gs/reflect/typefor.test.ts +31 -1
  107. package/gs/strings/meta.json +5 -2
  108. package/gs/strings/reader.test.ts +2 -2
  109. package/gs/strings/reader.ts +2 -2
  110. package/gs/sync/meta.json +1 -0
  111. package/gs/sync/sync.test.ts +41 -1
  112. package/gs/sync/sync.ts +41 -16
  113. package/gs/syscall/js/index.test.ts +18 -0
  114. package/gs/syscall/js/index.ts +12 -0
  115. package/gs/testing/testing.test.ts +32 -3
  116. package/gs/testing/testing.ts +13 -10
  117. package/package.json +1 -1
package/gs/embed/index.ts CHANGED
@@ -1,20 +1,244 @@
1
1
  import * as $ from '@goscript/builtin/index.js'
2
+ import * as io from '@goscript/io/index.js'
2
3
  import * as fs from '@goscript/io/fs/index.js'
4
+ import * as time from '@goscript/time/index.js'
3
5
 
4
6
  export class FS {
7
+ private files: Map<string, Uint8Array>
8
+
9
+ constructor(files?: Map<string, Uint8Array>) {
10
+ this.files = files ?? new Map()
11
+ }
12
+
13
+ clone(): FS {
14
+ const files = new Map<string, Uint8Array>()
15
+ for (const [name, data] of this.files) {
16
+ files.set(name, data.slice())
17
+ }
18
+ return $.markAsStructValue(new FS(files))
19
+ }
20
+
5
21
  Open(name: string): [fs.File, $.GoError] {
6
- return [null, pathError('open', name)]
22
+ const err = validatePath('open', name)
23
+ if (err != null) {
24
+ return [null, err]
25
+ }
26
+ const data = this.files.get(name)
27
+ if (data !== undefined) {
28
+ return [new embedFile(name, data), null]
29
+ }
30
+ const entries = this.dirEntries(name)
31
+ if (entries === null) {
32
+ return [null, pathError('open', name, fs.ErrNotExist)]
33
+ }
34
+ return [new embedFile(name, null, entries), null]
7
35
  }
8
36
 
9
37
  ReadDir(name: string): [$.Slice<fs.DirEntry>, $.GoError] {
10
- return [null, pathError('read', name)]
38
+ const err = validatePath('read', name)
39
+ if (err != null) {
40
+ return [null, err]
41
+ }
42
+ const entries = this.dirEntries(name)
43
+ if (entries === null) {
44
+ return [null, pathError('read', name, fs.ErrNotExist)]
45
+ }
46
+ return [entries, null]
11
47
  }
12
48
 
13
49
  ReadFile(name: string): [Uint8Array, $.GoError] {
14
- return [new Uint8Array(0), pathError('read', name)]
50
+ const err = validatePath('read', name)
51
+ if (err != null) {
52
+ return [new Uint8Array(0), err]
53
+ }
54
+ const data = this.files.get(name)
55
+ if (data === undefined) {
56
+ const err = this.dirExists(name) ? fs.ErrInvalid : fs.ErrNotExist
57
+ return [new Uint8Array(0), pathError('read', name, err)]
58
+ }
59
+ return [data.slice(), null]
60
+ }
61
+
62
+ Stat(name: string): [fs.FileInfo, $.GoError] {
63
+ const err = validatePath('stat', name)
64
+ if (err != null) {
65
+ return [null, err]
66
+ }
67
+ const data = this.files.get(name)
68
+ if (data !== undefined) {
69
+ return [new embedFileInfo(baseName(name), data.byteLength, 0o444), null]
70
+ }
71
+ if (!this.dirExists(name)) {
72
+ return [null, pathError('stat', name, fs.ErrNotExist)]
73
+ }
74
+ return [new embedFileInfo(baseName(name), 0, fs.ModeDir | 0o555), null]
75
+ }
76
+
77
+ private dirEntries(name: string): $.Slice<fs.DirEntry> | null {
78
+ if (!this.dirExists(name)) {
79
+ return null
80
+ }
81
+ const prefix = name === '.' ? '' : name + '/'
82
+ const entries = new Map<string, fs.DirEntry>()
83
+ for (const [filePath, data] of this.files) {
84
+ if (prefix !== '' && !filePath.startsWith(prefix)) {
85
+ continue
86
+ }
87
+ const rest = prefix === '' ? filePath : filePath.slice(prefix.length)
88
+ if (rest === '') {
89
+ continue
90
+ }
91
+ const slash = rest.indexOf('/')
92
+ const childName = slash === -1 ? rest : rest.slice(0, slash)
93
+ if (entries.has(childName)) {
94
+ continue
95
+ }
96
+ const isDir = slash !== -1
97
+ const info =
98
+ isDir ?
99
+ new embedFileInfo(childName, 0, fs.ModeDir | 0o555)
100
+ : new embedFileInfo(childName, data.byteLength, 0o444)
101
+ entries.set(childName, new embedDirEntry(info))
102
+ }
103
+ return Array.from(entries.values()).sort((a, b) =>
104
+ a!.Name().localeCompare(b!.Name()),
105
+ )
106
+ }
107
+
108
+ private dirExists(name: string): boolean {
109
+ if (name === '.') {
110
+ return true
111
+ }
112
+ const prefix = name + '/'
113
+ for (const path of this.files.keys()) {
114
+ if (path.startsWith(prefix)) {
115
+ return true
116
+ }
117
+ }
118
+ return false
119
+ }
120
+ }
121
+
122
+ class embedFile {
123
+ private offset = 0
124
+ private dirOffset = 0
125
+
126
+ constructor(
127
+ private readonly name: string,
128
+ private readonly data: Uint8Array | null,
129
+ private readonly entries: $.Slice<fs.DirEntry> = [],
130
+ ) {}
131
+
132
+ Close(): $.GoError {
133
+ return null
134
+ }
135
+
136
+ Read(buffer: Uint8Array): [number, $.GoError] {
137
+ if (this.data === null) {
138
+ return [0, pathError('read', this.name, fs.ErrInvalid)]
139
+ }
140
+ if (this.offset >= this.data.byteLength) {
141
+ return [0, io.EOF]
142
+ }
143
+ const n = Math.min(buffer.byteLength, this.data.byteLength - this.offset)
144
+ buffer.set(this.data.subarray(this.offset, this.offset + n))
145
+ this.offset += n
146
+ return [n, null]
147
+ }
148
+
149
+ ReadDir(n: number): [$.Slice<fs.DirEntry>, $.GoError] {
150
+ if (this.data !== null) {
151
+ return [null, pathError('readdir', this.name, fs.ErrInvalid)]
152
+ }
153
+ const allEntries = this.entries ?? []
154
+ if (n <= 0) {
155
+ const entries = allEntries.slice(this.dirOffset)
156
+ this.dirOffset = allEntries.length
157
+ return [entries, null]
158
+ }
159
+ if (this.dirOffset >= allEntries.length) {
160
+ return [[], io.EOF]
161
+ }
162
+ const entries = allEntries.slice(this.dirOffset, this.dirOffset + n)
163
+ this.dirOffset += entries.length
164
+ return [entries, null]
165
+ }
166
+
167
+ Stat(): [fs.FileInfo, $.GoError] {
168
+ if (this.data === null) {
169
+ return [new embedFileInfo(baseName(this.name), 0, fs.ModeDir | 0o555), null]
170
+ }
171
+ return [new embedFileInfo(baseName(this.name), this.data.byteLength, 0o444), null]
172
+ }
173
+ }
174
+
175
+ class embedFileInfo {
176
+ constructor(
177
+ private readonly name: string,
178
+ private readonly size: number,
179
+ private readonly mode: fs.FileMode,
180
+ ) {}
181
+
182
+ IsDir(): boolean {
183
+ return fs.FileMode_IsDir(this.mode)
184
+ }
185
+
186
+ ModTime(): time.Time {
187
+ return new time.Time()
188
+ }
189
+
190
+ Mode(): fs.FileMode {
191
+ return this.mode
192
+ }
193
+
194
+ Name(): string {
195
+ return this.name
196
+ }
197
+
198
+ Size(): number {
199
+ return this.size
200
+ }
201
+
202
+ Sys(): null {
203
+ return null
204
+ }
205
+ }
206
+
207
+ class embedDirEntry {
208
+ constructor(private readonly info: fs.FileInfo) {}
209
+
210
+ Info(): [fs.FileInfo, $.GoError] {
211
+ return [this.info, null]
212
+ }
213
+
214
+ IsDir(): boolean {
215
+ return this.info!.IsDir()
216
+ }
217
+
218
+ Name(): string {
219
+ return this.info!.Name()
220
+ }
221
+
222
+ Type(): fs.FileMode {
223
+ return fs.fileModeType(this.info!.Mode())
15
224
  }
16
225
  }
17
226
 
18
- function pathError(op: string, name: string): $.GoError {
19
- return new fs.PathError({ Op: op, Path: name, Err: fs.ErrNotExist })
227
+ function validatePath(op: string, name: string): $.GoError {
228
+ if (!fs.ValidPath(name)) {
229
+ return pathError(op, name, fs.ErrInvalid)
230
+ }
231
+ return null
232
+ }
233
+
234
+ function pathError(op: string, name: string, err: $.GoError): $.GoError {
235
+ return new fs.PathError({ Op: op, Path: name, Err: err })
236
+ }
237
+
238
+ function baseName(name: string): string {
239
+ const idx = name.lastIndexOf('/')
240
+ if (idx === -1) {
241
+ return name
242
+ }
243
+ return name.slice(idx + 1)
20
244
  }
@@ -58,6 +58,15 @@ describe('fmt basic value formatting', () => {
58
58
  expect(fmt.Sprintf('Type: %T', 3.14)).toBe('Type: float64')
59
59
  expect(fmt.Sprintf('Type: %T', 'hello')).toBe('Type: string')
60
60
  expect(fmt.Sprintf('Type: %T', true)).toBe('Type: bool')
61
+ expect(
62
+ fmt.Sprintf(
63
+ 'Type: %T',
64
+ $.namedValueInterfaceValue(123, 'int', {}, {
65
+ kind: $.TypeKind.Basic,
66
+ name: 'int',
67
+ }),
68
+ ),
69
+ ).toBe('Type: int')
61
70
  })
62
71
 
63
72
  it('%d truncation behavior including negatives', () => {
@@ -115,6 +124,20 @@ describe('fmt basic value formatting', () => {
115
124
  // We prefer GoString() first
116
125
  expect(fmt.Sprintf('%v', goStringer)).toBe('<go stringer>')
117
126
  })
127
+
128
+ it('%w formats errors by Error method', () => {
129
+ const err = $.newError('root')
130
+ expect(fmt.Errorf('wrap: %w', err)?.Error()).toBe('wrap: root')
131
+ })
132
+
133
+ it('%s formats stringers by String method', () => {
134
+ const stringer = {
135
+ String() {
136
+ return 'string-value'
137
+ },
138
+ }
139
+ expect(fmt.Sprintf('value=%s', stringer)).toBe('value=string-value')
140
+ })
118
141
  })
119
142
 
120
143
  describe('fmt spacing rules', () => {
@@ -146,7 +169,7 @@ describe('fmt spacing rules', () => {
146
169
  expect(output).toBe('hi there 1 2\n')
147
170
  })
148
171
 
149
- it('Fprint/Fprintln behave like Print/Println with writers', () => {
172
+ it('Fprint/Fprintln behave like Print/Println with writers', async () => {
150
173
  const chunks: Uint8Array[] = []
151
174
  const writer = {
152
175
  Write(b: Uint8Array): [number, any] {
@@ -155,14 +178,29 @@ describe('fmt spacing rules', () => {
155
178
  },
156
179
  }
157
180
 
158
- let [n, err] = fmt.Fprint(writer, 1, 2, 'x', 3)
181
+ let [n, err] = await fmt.Fprint(writer, 1, 2, 'x', 3)
159
182
  expect(err).toBeNull()
160
183
  expect(n).toBe(5) // "1 2x3".length
161
184
  expect(new TextDecoder().decode(chunks[0])).toBe('1 2x3')
162
- ;[, err] = fmt.Fprintln(writer, 'hi', 'there', 1, 2)
185
+ ;[, err] = await fmt.Fprintln(writer, 'hi', 'there', 1, 2)
163
186
  expect(err).toBeNull()
164
187
  expect(new TextDecoder().decode(chunks[1])).toBe('hi there 1 2\n')
165
188
  })
189
+
190
+ it('Fprintf awaits async writers', async () => {
191
+ const chunks: Uint8Array[] = []
192
+ const writer = {
193
+ async Write(b: Uint8Array): Promise<[number, any]> {
194
+ chunks.push(b)
195
+ return [b.length, null]
196
+ },
197
+ }
198
+
199
+ const [n, err] = await fmt.Fprintf(writer, 'n=%d s=%s', 7, 'ok')
200
+ expect(err).toBeNull()
201
+ expect(n).toBe(8)
202
+ expect(new TextDecoder().decode(chunks[0])).toBe('n=7 s=ok')
203
+ })
166
204
  })
167
205
 
168
206
  describe('fmt parseFormat basic cases', () => {
package/gs/fmt/fmt.ts CHANGED
@@ -34,15 +34,20 @@ function formatValue(value: any, verb: string): string {
34
34
  switch (verb) {
35
35
  case 'v': // default format
36
36
  return defaultFormat(value)
37
+ case 'w': // wrapped error
38
+ return defaultFormat(value)
37
39
  case 'd': // decimal integer
38
40
  return String(Math.trunc(Number(value)))
39
41
  case 'f': // decimal point, no exponent
40
42
  return Number(value).toString()
41
43
  case 's': // string
42
- return String(value)
44
+ if (typeof value === 'string') return value
45
+ if (value instanceof Uint8Array) return $.bytesToString(value)
46
+ return defaultFormat(value)
43
47
  case 't': // boolean
44
48
  return value ? 'true' : 'false'
45
49
  case 'T': // type (approximate Go names for primitives we need)
50
+ if (hasGoTypeName(value)) return value.__goType
46
51
  if (typeof value === 'number') {
47
52
  return Number.isInteger(value) ? 'int' : 'float64'
48
53
  }
@@ -85,6 +90,14 @@ function formatValue(value: any, verb: string): string {
85
90
  }
86
91
  }
87
92
 
93
+ function hasGoTypeName(value: unknown): value is { __goType: string } {
94
+ return (
95
+ value !== null &&
96
+ typeof value === 'object' &&
97
+ typeof (value as { __goType?: unknown }).__goType === 'string'
98
+ )
99
+ }
100
+
88
101
  function defaultFormat(value: any): string {
89
102
  if (value === null || value === undefined) return '<nil>'
90
103
  if (typeof value === 'boolean') return value ? 'true' : 'false'
@@ -120,6 +133,9 @@ function defaultFormat(value: any): string {
120
133
  // Ignore error by continuing to next case.
121
134
  }
122
135
  }
136
+ if ('__goValue' in value) {
137
+ return defaultFormat((value as { __goValue: unknown }).__goValue)
138
+ }
123
139
  // Basic Map/Set rendering
124
140
  if (value instanceof Map) {
125
141
  const parts: string[] = []
@@ -357,8 +373,21 @@ export function Sprintln(...a: any[]): string {
357
373
  return a.map(defaultFormat).join(' ') + '\n'
358
374
  }
359
375
 
376
+ async function writeToWriter(
377
+ w: any,
378
+ out: string,
379
+ ): Promise<[number, $.GoError | null]> {
380
+ if (w && w.Write) {
381
+ return await w.Write(new TextEncoder().encode(out))
382
+ }
383
+ return [0, $.newError('Writer does not implement Write method')]
384
+ }
385
+
360
386
  // Fprint functions (write to Writer) - simplified implementation
361
- export function Fprint(w: any, ...a: any[]): [number, $.GoError | null] {
387
+ export async function Fprint(
388
+ w: any,
389
+ ...a: any[]
390
+ ): Promise<[number, $.GoError | null]> {
362
391
  // Same spacing as Print
363
392
  let out = ''
364
393
  for (let i = 0; i < a.length; i++) {
@@ -369,32 +398,26 @@ export function Fprint(w: any, ...a: any[]): [number, $.GoError | null] {
369
398
  }
370
399
  out += defaultFormat(a[i])
371
400
  }
372
- if (w && w.Write) {
373
- return w.Write(new TextEncoder().encode(out))
374
- }
375
- return [0, $.newError('Writer does not implement Write method')]
401
+ return await writeToWriter(w, out)
376
402
  }
377
403
 
378
- export function Fprintf(
404
+ export async function Fprintf(
379
405
  w: any,
380
406
  format: string,
381
407
  ...a: any[]
382
- ): [number, $.GoError | null] {
408
+ ): Promise<[number, $.GoError | null]> {
383
409
  const result = parseFormat(format, a)
384
- if (w && w.Write) {
385
- return w.Write(new TextEncoder().encode(result))
386
- }
387
- return [0, $.newError('Writer does not implement Write method')]
410
+ return await writeToWriter(w, result)
388
411
  }
389
412
 
390
- export function Fprintln(w: any, ...a: any[]): [number, $.GoError | null] {
413
+ export async function Fprintln(
414
+ w: any,
415
+ ...a: any[]
416
+ ): Promise<[number, $.GoError | null]> {
391
417
  // Same behavior as Println
392
418
  const body = a.map(defaultFormat).join(' ')
393
419
  const result = body + '\n'
394
- if (w && w.Write) {
395
- return w.Write(new TextEncoder().encode(result))
396
- }
397
- return [0, $.newError('Writer does not implement Write method')]
420
+ return await writeToWriter(w, result)
398
421
  }
399
422
 
400
423
  // Append functions (append to byte slice)
package/gs/fmt/meta.json CHANGED
@@ -1,5 +1,10 @@
1
1
  {
2
+ "asyncFunctions": {
3
+ "Fprint": true,
4
+ "Fprintf": true,
5
+ "Fprintln": true
6
+ },
2
7
  "dependencies": [
3
8
  "errors"
4
9
  ]
5
- }
10
+ }
@@ -1,8 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import * as context from '@goscript/context/index.js'
4
+ import * as $ from '@goscript/builtin/index.js'
4
5
 
5
- import { MuxedConn, NewWebSocketConn } from './index.js'
6
+ import { MuxedConn, NewClientSet, NewPrefixClient, NewWebSocketConn } from './index.js'
6
7
 
7
8
  describe('starpc/srpc override', () => {
8
9
  it('wraps websocket connections with the same outbound direction as Go', () => {
@@ -28,4 +29,10 @@ describe('starpc/srpc override', () => {
28
29
  expect(serverConn?.outbound).toBe(false)
29
30
  expect(clientConn?.outbound).toBe(true)
30
31
  })
32
+
33
+ it('registers prefixed clients as srpc.Client values', () => {
34
+ const client = NewPrefixClient(NewClientSet(null), ['plugin-host/'])
35
+
36
+ expect($.typeAssert(client, 'srpc.Client').ok).toBe(true)
37
+ })
31
38
  })
@@ -115,6 +115,33 @@ export interface Client {
115
115
  ): Promise<[Stream | null, $.GoError]>
116
116
  }
117
117
 
118
+ $.registerInterfaceType('srpc.Client', null, [
119
+ {
120
+ name: 'ExecCall',
121
+ args: [
122
+ { name: 'ctx', type: 'context.Context' },
123
+ { name: 'service', type: { kind: $.TypeKind.Basic, name: 'string' } },
124
+ { name: 'method', type: { kind: $.TypeKind.Basic, name: 'string' } },
125
+ { name: 'in', type: 'srpc.Message' },
126
+ { name: 'out', type: 'srpc.Message' },
127
+ ],
128
+ returns: [{ name: '_r0', type: 'error' }],
129
+ },
130
+ {
131
+ name: 'NewStream',
132
+ args: [
133
+ { name: 'ctx', type: 'context.Context' },
134
+ { name: 'service', type: { kind: $.TypeKind.Basic, name: 'string' } },
135
+ { name: 'method', type: { kind: $.TypeKind.Basic, name: 'string' } },
136
+ { name: 'firstMsg', type: 'srpc.Message' },
137
+ ],
138
+ returns: [
139
+ { name: '_r0', type: 'srpc.Stream' },
140
+ { name: '_r1', type: 'error' },
141
+ ],
142
+ },
143
+ ])
144
+
118
145
  export class ClientSet implements Client {
119
146
  private clients: $.Slice<Client | null>
120
147
 
@@ -395,6 +422,84 @@ class memoryStream implements Stream {
395
422
  }
396
423
  }
397
424
 
425
+ class streamQueue {
426
+ private queue: (Message | null)[] = []
427
+ private waiters: ((msg: Message | null, err: $.GoError) => void)[] = []
428
+ private closed = false
429
+
430
+ public send(msg: Message | null): $.GoError {
431
+ if (this.closed) {
432
+ return ErrCompleted
433
+ }
434
+ const waiter = this.waiters.shift()
435
+ if (waiter != null) {
436
+ waiter(msg, null)
437
+ return null
438
+ }
439
+ this.queue.push(msg)
440
+ return null
441
+ }
442
+
443
+ public recv(msg: Message | null): MaybePromise<$.GoError> {
444
+ const next = this.queue.shift()
445
+ if (next !== undefined) {
446
+ if (msg != null && next != null) {
447
+ Object.assign(msg, next)
448
+ }
449
+ return null
450
+ }
451
+ if (this.closed) {
452
+ return io.EOF
453
+ }
454
+ return new Promise<$.GoError>((resolve) => {
455
+ this.waiters.push((sent, err) => {
456
+ if (msg != null && sent != null) {
457
+ Object.assign(msg, sent)
458
+ }
459
+ resolve(err)
460
+ })
461
+ })
462
+ }
463
+
464
+ public close(): $.GoError {
465
+ this.closed = true
466
+ for (const waiter of this.waiters.splice(0)) {
467
+ waiter(null, io.EOF)
468
+ }
469
+ return null
470
+ }
471
+ }
472
+
473
+ class pairedMemoryStream implements Stream {
474
+ constructor(
475
+ private ctx: context.Context,
476
+ private incoming: streamQueue,
477
+ private outgoing: streamQueue,
478
+ ) {}
479
+
480
+ public Context(): context.Context {
481
+ return this.ctx
482
+ }
483
+
484
+ public MsgSend(msg: Message | null): $.GoError {
485
+ return this.outgoing.send(msg)
486
+ }
487
+
488
+ public MsgRecv(msg: Message | null): MaybePromise<$.GoError> {
489
+ return this.incoming.recv(msg)
490
+ }
491
+
492
+ public CloseSend(): $.GoError {
493
+ return this.outgoing.close()
494
+ }
495
+
496
+ public Close(): $.GoError {
497
+ const incomingErr = this.incoming.close()
498
+ const outgoingErr = this.outgoing.close()
499
+ return incomingErr ?? outgoingErr
500
+ }
501
+ }
502
+
398
503
  class streamWithClose implements Stream {
399
504
  constructor(
400
505
  private stream: Stream,
@@ -738,7 +843,7 @@ class transportClient implements Client {
738
843
  if (output != null) {
739
844
  output.Reset()
740
845
  }
741
- return await writer?.Close() ?? null
846
+ return (await writer?.Close()) ?? null
742
847
  }
743
848
 
744
849
  public async NewStream(
@@ -792,7 +897,9 @@ export function NewClient(openStream: OpenStreamFunc | null): Client {
792
897
  class invokerClient implements Client {
793
898
  constructor(
794
899
  private invoker: Invoker | null,
795
- private contextFn: ((ctx: context.Context) => context.Context) | null = null,
900
+ private contextFn:
901
+ | ((ctx: context.Context) => context.Context)
902
+ | null = null,
796
903
  ) {}
797
904
 
798
905
  public async ExecCall(
@@ -833,19 +940,36 @@ class invokerClient implements Client {
833
940
  if (this.invoker == null) {
834
941
  return [null, ErrNoAvailableClients]
835
942
  }
836
- const stream = new memoryStream(
837
- this.contextFn == null ? ctx : this.contextFn(ctx),
838
- firstMsg,
943
+ const streamCtx = this.contextFn == null ? ctx : this.contextFn(ctx)
944
+ const clientInput = new streamQueue()
945
+ const serverInput = new streamQueue()
946
+ const clientStream = new pairedMemoryStream(
947
+ streamCtx,
948
+ clientInput,
949
+ serverInput,
950
+ )
951
+ const serverStream = new pairedMemoryStream(
952
+ streamCtx,
953
+ serverInput,
954
+ clientInput,
839
955
  )
956
+ if (firstMsg != null) {
957
+ const err = serverInput.send(firstMsg)
958
+ if (err != null) {
959
+ return [null, err]
960
+ }
961
+ }
840
962
  const pending = Promise.resolve(
841
- this.invoker.InvokeMethod(service, method, stream),
963
+ this.invoker.InvokeMethod(service, method, serverStream),
842
964
  )
843
965
  pending.then(([handled, err]) => {
844
966
  if (!handled || err != null) {
845
- stream.Close()
967
+ clientStream.Close()
968
+ return
846
969
  }
970
+ serverStream.CloseSend()
847
971
  })
848
- return [stream, null]
972
+ return [clientStream, null]
849
973
  }
850
974
  }
851
975
 
@@ -936,7 +1060,9 @@ export class ServerRPC {
936
1060
  stream,
937
1061
  )
938
1062
  const callErr = err ?? (handled ? null : ErrUnimplemented)
939
- await this.writer?.WritePacket(NewCallDataPacket(null, false, true, callErr))
1063
+ await this.writer?.WritePacket(
1064
+ NewCallDataPacket(null, false, true, callErr),
1065
+ )
940
1066
  return callErr
941
1067
  }
942
1068
 
@@ -995,8 +1121,10 @@ export function NewServerPipe(server: Server | null): OpenStreamFunc {
995
1121
  _ctx: context.Context,
996
1122
  _msgHandler: PacketDataHandler,
997
1123
  _closeHandler: CloseHandler,
998
- ): [PacketWriter | null, $.GoError] => [new closedPacketWriter(), null]) as
999
- OpenStreamFunc
1124
+ ): [PacketWriter | null, $.GoError] => [
1125
+ new closedPacketWriter(),
1126
+ null,
1127
+ ]) as OpenStreamFunc
1000
1128
  if (server != null) {
1001
1129
  openStream.__server = server
1002
1130
  }