one 2.11.5 → 2.12.1

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 (199) hide show
  1. package/android/src/main/java/com/margelo/nitro/one/HybridOneAudio.kt +309 -124
  2. package/dist/cjs/cli/build.cjs +31 -33
  3. package/dist/cjs/cli/build.native.cjs +31 -33
  4. package/dist/cjs/cli/build.native.js +31 -33
  5. package/dist/cjs/cli/build.native.js.map +1 -1
  6. package/dist/cjs/cli/buildPage.cjs +11 -7
  7. package/dist/cjs/cli/buildPage.native.cjs +11 -7
  8. package/dist/cjs/cli/buildPage.native.js +11 -7
  9. package/dist/cjs/cli/buildPage.native.js.map +1 -1
  10. package/dist/cjs/cli/buildPageWorker.cjs +1 -1
  11. package/dist/cjs/cli/buildPageWorker.native.cjs +1 -1
  12. package/dist/cjs/cli/buildPageWorker.native.js +1 -1
  13. package/dist/cjs/cli/buildPageWorker.native.js.map +1 -1
  14. package/dist/cjs/cli-dev-memory.cjs +43 -0
  15. package/dist/cjs/cli-dev-memory.native.cjs +84 -0
  16. package/dist/cjs/cli-dev-memory.native.js +86 -0
  17. package/dist/cjs/cli-dev-memory.native.js.map +1 -0
  18. package/dist/cjs/headless-server.cjs +2 -5
  19. package/dist/cjs/headless-server.native.cjs +2 -5
  20. package/dist/cjs/headless-server.native.js +2 -5
  21. package/dist/cjs/headless-server.native.js.map +1 -1
  22. package/dist/cjs/platform/audio/index.cjs +83 -76
  23. package/dist/cjs/platform/audio/index.native.cjs +2 -2
  24. package/dist/cjs/platform/audio/index.native.js +2 -2
  25. package/dist/cjs/platform/audio/index.native.js.map +1 -1
  26. package/dist/cjs/platform/audio/nowPlaying.cjs +126 -0
  27. package/dist/cjs/platform/audio/nowPlaying.native.cjs +160 -0
  28. package/dist/cjs/platform/audio/nowPlaying.native.js +162 -0
  29. package/dist/cjs/platform/audio/nowPlaying.native.js.map +1 -0
  30. package/dist/cjs/platform/audio/player.cjs +15 -0
  31. package/dist/cjs/platform/audio/player.native.cjs +14 -0
  32. package/dist/cjs/platform/audio/player.native.js +14 -0
  33. package/dist/cjs/platform/audio/player.native.js.map +1 -1
  34. package/dist/cjs/platform/audio/unavailable.cjs +1 -1
  35. package/dist/cjs/platform/audio/unavailable.native.cjs +1 -1
  36. package/dist/cjs/platform/audio/unavailable.native.js +1 -1
  37. package/dist/cjs/platform/audio/unavailable.native.js.map +1 -1
  38. package/dist/cjs/serve-worker.cjs +0 -3
  39. package/dist/cjs/serve-worker.native.cjs +0 -3
  40. package/dist/cjs/serve-worker.native.js +0 -3
  41. package/dist/cjs/serve-worker.native.js.map +1 -1
  42. package/dist/cjs/server/oneServe.cjs +0 -6
  43. package/dist/cjs/server/oneServe.native.cjs +0 -6
  44. package/dist/cjs/server/oneServe.native.js +0 -6
  45. package/dist/cjs/server/oneServe.native.js.map +1 -1
  46. package/dist/cjs/server/workerHandler.cjs +12 -10
  47. package/dist/cjs/server/workerHandler.native.cjs +14 -10
  48. package/dist/cjs/server/workerHandler.native.js +14 -10
  49. package/dist/cjs/server/workerHandler.native.js.map +1 -1
  50. package/dist/cjs/vite/plugins/workerdDevPlugin.cjs +33 -37
  51. package/dist/cjs/vite/plugins/workerdDevPlugin.native.cjs +33 -37
  52. package/dist/cjs/vite/plugins/workerdDevPlugin.native.js +33 -37
  53. package/dist/cjs/vite/plugins/workerdDevPlugin.native.js.map +1 -1
  54. package/dist/esm/cli/build.mjs +31 -33
  55. package/dist/esm/cli/build.mjs.map +1 -1
  56. package/dist/esm/cli/build.native.js +31 -33
  57. package/dist/esm/cli/build.native.js.map +1 -1
  58. package/dist/esm/cli/buildPage.mjs +11 -7
  59. package/dist/esm/cli/buildPage.mjs.map +1 -1
  60. package/dist/esm/cli/buildPage.native.js +11 -7
  61. package/dist/esm/cli/buildPage.native.js.map +1 -1
  62. package/dist/esm/cli/buildPageWorker.mjs +1 -1
  63. package/dist/esm/cli/buildPageWorker.mjs.map +1 -1
  64. package/dist/esm/cli/buildPageWorker.native.js +1 -1
  65. package/dist/esm/cli/buildPageWorker.native.js.map +1 -1
  66. package/dist/esm/cli-dev-memory.mjs +23 -0
  67. package/dist/esm/cli-dev-memory.mjs.map +1 -0
  68. package/dist/esm/cli-dev-memory.native.js +62 -0
  69. package/dist/esm/cli-dev-memory.native.js.map +1 -0
  70. package/dist/esm/headless-server.mjs +2 -3
  71. package/dist/esm/headless-server.mjs.map +1 -1
  72. package/dist/esm/headless-server.native.js +2 -3
  73. package/dist/esm/headless-server.native.js.map +1 -1
  74. package/dist/esm/platform/audio/index.mjs +83 -76
  75. package/dist/esm/platform/audio/index.mjs.map +1 -1
  76. package/dist/esm/platform/audio/index.native.js +2 -2
  77. package/dist/esm/platform/audio/index.native.js.map +1 -1
  78. package/dist/esm/platform/audio/nowPlaying.mjs +102 -0
  79. package/dist/esm/platform/audio/nowPlaying.mjs.map +1 -0
  80. package/dist/esm/platform/audio/nowPlaying.native.js +134 -0
  81. package/dist/esm/platform/audio/nowPlaying.native.js.map +1 -0
  82. package/dist/esm/platform/audio/player.mjs +15 -0
  83. package/dist/esm/platform/audio/player.mjs.map +1 -1
  84. package/dist/esm/platform/audio/player.native.js +14 -0
  85. package/dist/esm/platform/audio/player.native.js.map +1 -1
  86. package/dist/esm/platform/audio/unavailable.mjs +1 -1
  87. package/dist/esm/platform/audio/unavailable.mjs.map +1 -1
  88. package/dist/esm/platform/audio/unavailable.native.js +1 -1
  89. package/dist/esm/platform/audio/unavailable.native.js.map +1 -1
  90. package/dist/esm/serve-worker.mjs +1 -2
  91. package/dist/esm/serve-worker.mjs.map +1 -1
  92. package/dist/esm/serve-worker.native.js +1 -2
  93. package/dist/esm/serve-worker.native.js.map +1 -1
  94. package/dist/esm/server/oneServe.mjs +0 -6
  95. package/dist/esm/server/oneServe.mjs.map +1 -1
  96. package/dist/esm/server/oneServe.native.js +0 -6
  97. package/dist/esm/server/oneServe.native.js.map +1 -1
  98. package/dist/esm/server/workerHandler.mjs +12 -10
  99. package/dist/esm/server/workerHandler.mjs.map +1 -1
  100. package/dist/esm/server/workerHandler.native.js +14 -10
  101. package/dist/esm/server/workerHandler.native.js.map +1 -1
  102. package/dist/esm/vite/plugins/workerdDevPlugin.mjs +33 -37
  103. package/dist/esm/vite/plugins/workerdDevPlugin.mjs.map +1 -1
  104. package/dist/esm/vite/plugins/workerdDevPlugin.native.js +33 -37
  105. package/dist/esm/vite/plugins/workerdDevPlugin.native.js.map +1 -1
  106. package/ios/Nitro/HybridOneAudio.swift +309 -96
  107. package/nitrogen/generated/android/c++/JAudioPlayOptions.hpp +61 -0
  108. package/nitrogen/generated/android/c++/JAudioRemoteCommandEvent.hpp +8 -3
  109. package/nitrogen/generated/android/c++/JFunc_void_AudioRemoteCommandEvent.hpp +1 -0
  110. package/nitrogen/generated/android/c++/JHybridOneAudioSpec.cpp +37 -3
  111. package/nitrogen/generated/android/c++/JHybridOneAudioSpec.hpp +3 -1
  112. package/nitrogen/generated/android/kotlin/com/margelo/nitro/one/AudioPlayOptions.kt +56 -0
  113. package/nitrogen/generated/android/kotlin/com/margelo/nitro/one/AudioRemoteCommandEvent.kt +9 -4
  114. package/nitrogen/generated/android/kotlin/com/margelo/nitro/one/HybridOneAudioSpec.kt +40 -32
  115. package/nitrogen/generated/ios/One-Swift-Cxx-Umbrella.hpp +3 -0
  116. package/nitrogen/generated/ios/c++/HybridOneAudioSpecSwift.hpp +21 -2
  117. package/nitrogen/generated/ios/swift/AudioPlayOptions.swift +60 -0
  118. package/nitrogen/generated/ios/swift/AudioRemoteCommandEvent.swift +19 -1
  119. package/nitrogen/generated/ios/swift/HybridOneAudioSpec.swift +3 -1
  120. package/nitrogen/generated/ios/swift/HybridOneAudioSpec_cxx.swift +70 -32
  121. package/nitrogen/generated/shared/c++/AudioPlayOptions.hpp +87 -0
  122. package/nitrogen/generated/shared/c++/AudioRemoteCommandEvent.hpp +7 -2
  123. package/nitrogen/generated/shared/c++/HybridOneAudioSpec.cpp +2 -0
  124. package/nitrogen/generated/shared/c++/HybridOneAudioSpec.hpp +6 -1
  125. package/package.json +13 -13
  126. package/run.mjs +5 -1
  127. package/src/cli/build.ts +35 -32
  128. package/src/cli/buildPage.test.ts +152 -0
  129. package/src/cli/buildPage.ts +14 -9
  130. package/src/cli/buildPageWorker.ts +2 -1
  131. package/src/cli/workerPool.ts +1 -0
  132. package/src/cli-dev-memory.ts +24 -0
  133. package/src/headless-server.ts +2 -2
  134. package/src/index.ts +1 -0
  135. package/src/platform/audio/index.native.ts +4 -2
  136. package/src/platform/audio/index.ts +102 -74
  137. package/src/platform/audio/nowPlaying.ts +122 -0
  138. package/src/platform/audio/player.native.ts +17 -1
  139. package/src/platform/audio/player.ts +23 -1
  140. package/src/platform/audio/types.ts +6 -1
  141. package/src/platform/audio/unavailable.ts +3 -1
  142. package/src/platform/index.native.ts +1 -0
  143. package/src/platform/index.ts +1 -0
  144. package/src/platform/specs/OneAudio.nitro.ts +11 -1
  145. package/src/serve-worker.ts +2 -3
  146. package/src/server/oneServe.ts +0 -6
  147. package/src/server/workerHandler.ts +34 -18
  148. package/src/vite/plugins/workerdDevPlugin.ts +33 -37
  149. package/src/vite/resolveResponse.ts +2 -2
  150. package/types/cli/build.d.ts.map +1 -1
  151. package/types/cli/buildPage.d.ts +1 -1
  152. package/types/cli/buildPage.d.ts.map +1 -1
  153. package/types/cli/workerPool.d.ts +1 -0
  154. package/types/cli/workerPool.d.ts.map +1 -1
  155. package/types/cli-dev-memory.d.ts +2 -0
  156. package/types/cli-dev-memory.d.ts.map +1 -0
  157. package/types/headless-server.d.ts +3 -3
  158. package/types/headless-server.d.ts.map +1 -1
  159. package/types/index.d.ts +1 -1
  160. package/types/index.d.ts.map +1 -1
  161. package/types/platform/audio/index.d.ts +2 -2
  162. package/types/platform/audio/index.d.ts.map +1 -1
  163. package/types/platform/audio/index.native.d.ts +3 -3
  164. package/types/platform/audio/index.native.d.ts.map +1 -1
  165. package/types/platform/audio/nowPlaying.d.ts +12 -0
  166. package/types/platform/audio/nowPlaying.d.ts.map +1 -0
  167. package/types/platform/audio/player.d.ts +3 -1
  168. package/types/platform/audio/player.d.ts.map +1 -1
  169. package/types/platform/audio/player.native.d.ts +3 -1
  170. package/types/platform/audio/player.native.d.ts.map +1 -1
  171. package/types/platform/audio/types.d.ts +3 -1
  172. package/types/platform/audio/types.d.ts.map +1 -1
  173. package/types/platform/audio/unavailable.d.ts +3 -3
  174. package/types/platform/audio/unavailable.d.ts.map +1 -1
  175. package/types/platform/index.d.ts +1 -1
  176. package/types/platform/index.d.ts.map +1 -1
  177. package/types/platform/index.native.d.ts +1 -1
  178. package/types/platform/index.native.d.ts.map +1 -1
  179. package/types/platform/specs/OneAudio.nitro.d.ts +8 -1
  180. package/types/platform/specs/OneAudio.nitro.d.ts.map +1 -1
  181. package/types/serve-worker.d.ts +3 -3
  182. package/types/serve-worker.d.ts.map +1 -1
  183. package/types/server/oneServe.d.ts.map +1 -1
  184. package/types/server/workerHandler.d.ts +1 -1
  185. package/types/server/workerHandler.d.ts.map +1 -1
  186. package/types/vite/plugins/workerdDevPlugin.d.ts.map +1 -1
  187. package/types/vite/resolveResponse.d.ts +1 -1
  188. package/types/vite/resolveResponse.d.ts.map +1 -1
  189. package/dist/cjs/server/staticHtmlFetcher.cjs +0 -33
  190. package/dist/cjs/server/staticHtmlFetcher.native.cjs +0 -35
  191. package/dist/cjs/server/staticHtmlFetcher.native.js +0 -37
  192. package/dist/cjs/server/staticHtmlFetcher.native.js.map +0 -1
  193. package/dist/esm/server/staticHtmlFetcher.mjs +0 -10
  194. package/dist/esm/server/staticHtmlFetcher.mjs.map +0 -1
  195. package/dist/esm/server/staticHtmlFetcher.native.js +0 -10
  196. package/dist/esm/server/staticHtmlFetcher.native.js.map +0 -1
  197. package/src/server/staticHtmlFetcher.ts +0 -12
  198. package/types/server/staticHtmlFetcher.d.ts +0 -3
  199. package/types/server/staticHtmlFetcher.d.ts.map +0 -1
@@ -0,0 +1,152 @@
1
+ import { mkdtemp, readFile, rm } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, expect, it } from 'vitest'
5
+ import {
6
+ LOADER_JS_POSTFIX,
7
+ NATIVE_LOADER_JS_POSTFIX_UNCACHED,
8
+ } from '../constants'
9
+ import { buildPage } from './buildPage'
10
+
11
+ let root: string | undefined
12
+
13
+ afterEach(async () => {
14
+ if (root) await rm(root, { recursive: true, force: true })
15
+ })
16
+
17
+ async function fixture() {
18
+ root = await mkdtemp(join(tmpdir(), 'one-build-page-'))
19
+ const serverEntry = join(root, 'entry.mjs')
20
+ const { writeFile, mkdir } = await import('node:fs/promises')
21
+ await writeFile(
22
+ serverEntry,
23
+ `export default {
24
+ options: { routes: {
25
+ '/app/probe.tsx': async () => ({
26
+ loader: ({ params }) => ({ slug: params.slug }),
27
+ }),
28
+ '/app/gated.tsx': async () => ({
29
+ loader: () => { throw Response.redirect('http://example.com/login', 302) },
30
+ }),
31
+ } },
32
+ render: async ({ loaderData }) => '<html><head></head><body>' + JSON.stringify(loaderData) + '</body></html>'
33
+ }`
34
+ )
35
+ return serverEntry
36
+ }
37
+
38
+ async function buildRoute(
39
+ serverEntry: string,
40
+ out: string,
41
+ route: { path: string; file: string; params: any },
42
+ skipNativeLoaders?: boolean
43
+ ) {
44
+ const clientDir = join(out, 'client')
45
+ const staticDir = join(out, 'static')
46
+ const { mkdir } = await import('node:fs/promises')
47
+ await mkdir(join(clientDir, 'assets'), { recursive: true })
48
+ await mkdir(staticDir, { recursive: true })
49
+ const built = await buildPage(
50
+ serverEntry,
51
+ 'app',
52
+ route.path,
53
+ '/probe.tsx',
54
+ route.params,
55
+ { type: 'ssg', file: route.file, layouts: [], middlewares: [] } as any,
56
+ { file: 'assets/probe.js' },
57
+ staticDir,
58
+ clientDir,
59
+ {},
60
+ 'server/probe.js',
61
+ [],
62
+ [],
63
+ [],
64
+ [],
65
+ undefined,
66
+ undefined,
67
+ undefined,
68
+ undefined,
69
+ undefined,
70
+ skipNativeLoaders
71
+ )
72
+ return { built, clientDir, staticDir }
73
+ }
74
+
75
+ function nativeTwins(clientDir: string, loaderPath: string) {
76
+ const cached = join(clientDir, loaderPath.replace(/\.js$/, '.native.js'))
77
+ const uncached = join(
78
+ clientDir,
79
+ loaderPath.replace(LOADER_JS_POSTFIX, NATIVE_LOADER_JS_POSTFIX_UNCACHED)
80
+ )
81
+ return { cached, uncached }
82
+ }
83
+
84
+ async function exists(path: string) {
85
+ try {
86
+ await readFile(path)
87
+ return true
88
+ } catch {
89
+ return false
90
+ }
91
+ }
92
+
93
+ it('omits native loader twins when skipped, keeping web output identical', async () => {
94
+ const serverEntry = await fixture()
95
+ for (const route of [
96
+ { path: '/probe', file: './probe.tsx', params: { slug: 'probe' } },
97
+ { path: '/gated', file: './gated.tsx', params: {} },
98
+ ]) {
99
+ const enabled = await buildRoute(serverEntry, join(root!, 'enabled', route.path), route)
100
+ const skipped = await buildRoute(
101
+ serverEntry,
102
+ join(root!, 'skipped', route.path),
103
+ route,
104
+ true
105
+ )
106
+
107
+ // web loader and html are byte-identical either way
108
+ const [enabledLoader, skippedLoader] = await Promise.all([
109
+ readFile(join(enabled.clientDir, enabled.built.loaderPath), 'utf8'),
110
+ readFile(join(skipped.clientDir, skipped.built.loaderPath), 'utf8'),
111
+ ])
112
+ expect(skippedLoader, route.path).toBe(enabledLoader)
113
+ const [enabledHtml, skippedHtml] = await Promise.all([
114
+ readFile(join(enabled.staticDir, `${route.path.slice(1)}.html`), 'utf8'),
115
+ readFile(join(skipped.staticDir, `${route.path.slice(1)}.html`), 'utf8'),
116
+ ])
117
+ expect(skippedHtml, route.path).toBe(enabledHtml)
118
+
119
+ // native twins exist by default, absent when skipped
120
+ const enabledTwins = nativeTwins(enabled.clientDir, enabled.built.loaderPath)
121
+ expect(await exists(enabledTwins.cached), `${route.path} cached`).toBe(true)
122
+ expect(await exists(enabledTwins.uncached), `${route.path} uncached`).toBe(true)
123
+ const skippedTwins = nativeTwins(skipped.clientDir, skipped.built.loaderPath)
124
+ expect(await exists(skippedTwins.cached), `${route.path} cached`).toBe(false)
125
+ expect(await exists(skippedTwins.uncached), `${route.path} uncached`).toBe(false)
126
+ }
127
+
128
+ // the redirect loader still carries its signal on the web file
129
+ const skipped = await buildRoute(
130
+ serverEntry,
131
+ join(root!, 'redirect'),
132
+ { path: '/gated', file: './gated.tsx', params: {} },
133
+ true
134
+ )
135
+ const webLoader = await readFile(
136
+ join(skipped.clientDir, skipped.built.loaderPath),
137
+ 'utf8'
138
+ )
139
+ expect(webLoader).toContain('__oneRedirect')
140
+ })
141
+
142
+ it('writes native loader twins by default', async () => {
143
+ const serverEntry = await fixture()
144
+ const { built, clientDir } = await buildRoute(
145
+ serverEntry,
146
+ join(root!, 'default'),
147
+ { path: '/probe', file: './probe.tsx', params: { slug: 'probe' } }
148
+ )
149
+ const twins = nativeTwins(clientDir, built.loaderPath)
150
+ expect(await exists(twins.cached)).toBe(true)
151
+ expect(await exists(twins.uncached)).toBe(true)
152
+ })
@@ -63,7 +63,8 @@ export async function buildPage(
63
63
  criticalPreloads?: string[],
64
64
  deferredPreloads?: string[],
65
65
  useAfterLCP?: boolean,
66
- useAfterLCPAggressive?: boolean
66
+ useAfterLCPAggressive?: boolean,
67
+ skipNativeLoaders?: boolean
67
68
  ): Promise<One.RouteBuildInfo> {
68
69
  let t0 = performance.now()
69
70
 
@@ -275,10 +276,12 @@ prefetchCSS()
275
276
  loaderPartialPath,
276
277
  `export function loader(){return ${redirectData}}`
277
278
  )
278
- // native-friendly CJS version
279
- const nativeCjs = `exports.loader = function(){return ${redirectData}}`
280
- await outputFile(loaderPartialPath.replace(/\.js$/, '.native.js'), nativeCjs)
281
- await outputFile(uncachedNativePath, nativeCjs)
279
+ if (!skipNativeLoaders) {
280
+ // native-friendly CJS version
281
+ const nativeCjs = `exports.loader = function(){return ${redirectData}}`
282
+ await outputFile(loaderPartialPath.replace(/\.js$/, '.native.js'), nativeCjs)
283
+ await outputFile(uncachedNativePath, nativeCjs)
284
+ }
282
285
  loaderPath = getLoaderPath(path)
283
286
  loaderData = {}
284
287
  } else {
@@ -289,10 +292,12 @@ prefetchCSS()
289
292
  // stub of its own to replace)
290
293
  const data = JSON.stringify(loaderData)
291
294
  await outputFile(loaderPartialPath, `export function loader(){return ${data}}`)
292
- // native-friendly CJS version
293
- const nativeCjs = `exports.loader = function(){return ${data}}`
294
- await outputFile(loaderPartialPath.replace(/\.js$/, '.native.js'), nativeCjs)
295
- await outputFile(uncachedNativePath, nativeCjs)
295
+ if (!skipNativeLoaders) {
296
+ // native-friendly CJS version
297
+ const nativeCjs = `exports.loader = function(){return ${data}}`
298
+ await outputFile(loaderPartialPath.replace(/\.js$/, '.native.js'), nativeCjs)
299
+ await outputFile(uncachedNativePath, nativeCjs)
300
+ }
296
301
  loaderPath = getLoaderPath(path)
297
302
  }
298
303
  }
@@ -86,7 +86,8 @@ parentPort.on('message', async (msg: any) => {
86
86
  msg.args.criticalPreloads,
87
87
  msg.args.deferredPreloads,
88
88
  msg.args.useAfterLCP,
89
- msg.args.useAfterLCPAggressive
89
+ msg.args.useAfterLCPAggressive,
90
+ msg.args.skipNativeLoaders
90
91
  )
91
92
  })
92
93
  // loader data is already in the artifacts and is discarded by the parent.
@@ -127,6 +127,7 @@ export class BuildWorkerPool {
127
127
  deferredPreloads?: string[]
128
128
  useAfterLCP?: boolean
129
129
  useAfterLCPAggressive?: boolean
130
+ skipNativeLoaders?: boolean
130
131
  }): Promise<any> {
131
132
  if (this._terminated) {
132
133
  throw new Error('Worker pool has been terminated')
@@ -0,0 +1,24 @@
1
+ import { createRequire } from 'node:module'
2
+ import { pathToFileURL } from 'node:url'
3
+
4
+ // rolldown's allocator otherwise keeps large arenas resident after prebundling.
5
+ // initialize its binding with direct allocations, then restore the environment
6
+ // before user config and child commands run (bun has its own allocator).
7
+ export async function initializeDevAllocator() {
8
+ const options = {
9
+ MIMALLOC_DISALLOW_ARENA_ALLOC: '1',
10
+ MIMALLOC_PURGE_DELAY: '0',
11
+ }
12
+ const previous = new Map(Object.keys(options).map((key) => [key, process.env[key]]))
13
+ for (const [key, value] of Object.entries(options)) process.env[key] ??= value
14
+
15
+ try {
16
+ const requireFromVite = createRequire(import.meta.resolve('vite'))
17
+ await import(pathToFileURL(requireFromVite.resolve('rolldown')).href)
18
+ } finally {
19
+ for (const [key, value] of previous) {
20
+ if (value === undefined) delete process.env[key]
21
+ else process.env[key] = value
22
+ }
23
+ }
24
+ }
@@ -27,13 +27,13 @@
27
27
  * })
28
28
  *
29
29
  * const response = await server.fetch(request)
30
+ * // pass a request-scoped static HTML reader as the fourth argument for SPA or SSG
30
31
  * server.updateRoutes(newManifest, newRoutes)
31
32
  */
32
33
 
33
34
  export { createWorkerHandler, type LazyRoutes } from './server/workerHandler'
34
35
  export { compileManifest } from './createHandleRequest'
35
36
  export type { RequestHandlers } from './createHandleRequest'
36
- export { getFetchStaticHtml, setFetchStaticHtml } from './server/staticHtmlFetcher'
37
37
  export type { One } from './vite/types'
38
38
 
39
39
  import { setServerGlobals } from './server/setServerGlobals'
@@ -92,7 +92,7 @@ export async function createServer(options: HeadlessServerOptions) {
92
92
  })
93
93
 
94
94
  return {
95
- /** handle an incoming request — returns Response or null (unmatched) */
95
+ /** handle a request with an optional request-scoped static HTML reader */
96
96
  fetch: handler.handleRequest,
97
97
 
98
98
  /** hot-swap the route table without recreating the server */
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export {
37
37
  export type {
38
38
  PortalProps,
39
39
  PortalHostProps,
40
+ AudioPlayOptions,
40
41
  AudioPlayer,
41
42
  AudioPlayerOptions,
42
43
  AudioPlayerSubscription,
@@ -2,6 +2,7 @@ import { validateCallback } from '../validateCallback'
2
2
  import { rethrowNativeError } from '../nativeError'
3
3
  import type {
4
4
  AudioPlaybackStatus,
5
+ AudioPlayOptions,
5
6
  AudioRecordingPermission,
6
7
  AudioRecordingResult,
7
8
  AudioRecordingStatus,
@@ -15,6 +16,7 @@ import { AudioPlayer, createPlayer } from './player.native'
15
16
  export type {
16
17
  AudioPlaybackState,
17
18
  AudioPlaybackStatus,
19
+ AudioPlayOptions,
18
20
  AudioRecordingPermission,
19
21
  AudioRecordingResult,
20
22
  AudioRecordingState,
@@ -33,8 +35,8 @@ export const Audio = Object.freeze({
33
35
  native().getRecordingPermissionStatus().catch(rethrowNativeError),
34
36
  requestRecordingPermission: (): Promise<AudioRecordingPermission> =>
35
37
  native().requestRecordingPermission().catch(rethrowNativeError),
36
- play: (uri: string): Promise<AudioPlaybackStatus> =>
37
- native().play(uri).catch(rethrowNativeError),
38
+ play: (uri: string, options?: AudioPlayOptions): Promise<AudioPlaybackStatus> =>
39
+ native().play(uri, options ?? {}).catch(rethrowNativeError),
38
40
  getPlaybackStatus: (): Promise<AudioPlaybackStatus> =>
39
41
  native().getPlaybackStatus().catch(rethrowNativeError),
40
42
  pause: (): Promise<AudioPlaybackStatus> => native().pause().catch(rethrowNativeError),
@@ -2,17 +2,24 @@ import { Audio as unavailable } from './unavailable'
2
2
  import { validateCallback } from '../validateCallback'
3
3
  import type {
4
4
  AudioPlaybackStatus,
5
+ AudioPlayOptions,
5
6
  AudioRecordingPermission,
6
7
  AudioRecordingResult,
7
8
  AudioRecordingStatus,
8
9
  AudioNowPlayingInfo,
9
10
  AudioRemoteCommandEvent,
10
11
  } from '../specs/OneAudio.nitro'
12
+ import { addRemoteListener, claimNowPlaying, releaseNowPlaying } from './nowPlaying'
11
13
  import { AudioPlayer, createPlayer } from './player'
12
14
  export type { PitchCorrectionQuality } from '../specs/OneAudio.nitro'
13
15
  export type * from './types'
14
16
  export type * from './unavailable'
15
17
  let player: HTMLAudioElement | undefined
18
+ // a play with a position or paused flag, between creation and the landed
19
+ // seek. paused is the intent pause and resume update while it loads.
20
+ let pendingLoad:
21
+ | { audio: HTMLAudioElement; paused: boolean; cancel?: () => void }
22
+ | undefined
16
23
  let playbackError: string | undefined
17
24
  let permission: AudioRecordingPermission = 'undetermined'
18
25
  let openingRecording = false
@@ -27,8 +34,6 @@ let recording:
27
34
  error?: Error
28
35
  }
29
36
  | undefined
30
- const remoteListeners = new Set<(event: AudioRemoteCommandEvent) => void>()
31
- let remoteInstalled = false
32
37
 
33
38
  async function getRecordingPermissionStatus(): Promise<AudioRecordingPermission> {
34
39
  if (typeof window === 'undefined' || !navigator.mediaDevices?.getUserMedia)
@@ -74,7 +79,7 @@ function playbackStatus(): AudioPlaybackStatus {
74
79
  ? 'failed'
75
80
  : player.ended
76
81
  ? 'ended'
77
- : player.readyState < 2
82
+ : pendingLoad || player.readyState < 2
78
83
  ? 'loading'
79
84
  : player.paused
80
85
  ? 'paused'
@@ -90,7 +95,9 @@ function requirePlayer(): HTMLAudioElement {
90
95
  return player
91
96
  }
92
97
  function clearPlayer(): void {
93
- clearNowPlaying()
98
+ releaseNowPlaying('global')
99
+ pendingLoad?.cancel?.()
100
+ pendingLoad = undefined
94
101
  if (player) {
95
102
  player.pause()
96
103
  player.removeAttribute('src')
@@ -152,69 +159,88 @@ function recorderChange(event: 'pause' | 'resume'): Promise<AudioRecordingStatus
152
159
  }
153
160
  })
154
161
  }
155
- function mediaSession(): MediaSession {
156
- if (!navigator.mediaSession) throw new Error('Audio: Media Session is unavailable')
157
- return navigator.mediaSession
158
- }
159
- function clearNowPlaying(): void {
160
- if (typeof window === 'undefined' || !navigator.mediaSession) return
161
- const session = navigator.mediaSession
162
- if (remoteInstalled)
163
- for (const action of ['play', 'pause', 'seekto'] as const)
164
- session.setActionHandler(action, null)
165
- remoteInstalled = false
166
- session.metadata = null
167
- session.playbackState = 'none'
168
- }
169
- function syncNowPlaying(): void {
170
- if (!navigator.mediaSession?.metadata || !player) return
171
- navigator.mediaSession.playbackState =
172
- player.paused || player.ended ? 'paused' : 'playing'
173
- }
174
- async function remote(event: AudioRemoteCommandEvent): Promise<void> {
175
- if (!player || !navigator.mediaSession?.metadata) return
176
- const audio = player
177
- try {
178
- if (event.type === 'play') await Audio.resume()
179
- else if (event.type === 'pause') await Audio.pause()
180
- else await Audio.seek(event.positionMs!)
181
- } catch {
182
- // the browser callback has no error channel; failed work emits no success.
183
- return
184
- }
185
- if (player !== audio) return
186
- syncNowPlaying()
187
- for (const listener of [...remoteListeners]) listener(event)
188
- }
189
- function installRemote(): void {
190
- if (remoteInstalled) return
191
- const session = mediaSession()
192
- session.setActionHandler('play', () => remote({ type: 'play' }))
193
- session.setActionHandler('pause', () => remote({ type: 'pause' }))
194
- session.setActionHandler('seekto', (event) =>
195
- remote({ type: 'seek', positionMs: (event.seekTime ?? 0) * 1000 })
196
- )
197
- remoteInstalled = true
162
+ // resolves once the element has metadata and sits at the position, without
163
+ // playing. replacement cancels it through the load's cancel hook.
164
+ function prepareAt(load: NonNullable<typeof pendingLoad>, positionMs: number): Promise<void> {
165
+ const { audio } = load
166
+ return new Promise((resolve, reject) => {
167
+ const cleanup = () => {
168
+ audio.removeEventListener('loadedmetadata', ready)
169
+ audio.removeEventListener('seeked', done)
170
+ audio.removeEventListener('error', failed)
171
+ load.cancel = undefined
172
+ }
173
+ let seconds = 0
174
+ // a source without range support lands somewhere else, often 0; fail
175
+ // rather than let a later resume play the opening.
176
+ const done = () => {
177
+ cleanup()
178
+ if (Math.abs(audio.currentTime - seconds) > 0.25)
179
+ reject(new Error('Audio.play: the source cannot seek to the position'))
180
+ else resolve()
181
+ }
182
+ const failed = () => {
183
+ cleanup()
184
+ reject(new Error(`Audio.play: ${audio.error?.message || 'decoding failed'}`))
185
+ }
186
+ function ready() {
187
+ audio.removeEventListener('loadedmetadata', ready)
188
+ seconds = Math.min(
189
+ positionMs / 1000,
190
+ Number.isFinite(audio.duration) ? audio.duration : Infinity
191
+ )
192
+ if (audio.currentTime === seconds) return done()
193
+ audio.addEventListener('seeked', done, { once: true })
194
+ audio.currentTime = seconds
195
+ }
196
+ load.cancel = () => {
197
+ cleanup()
198
+ reject(new Error('Audio.play: playback was replaced'))
199
+ }
200
+ audio.addEventListener('error', failed, { once: true })
201
+ if (audio.readyState >= 1) ready()
202
+ else audio.addEventListener('loadedmetadata', ready, { once: true })
203
+ })
198
204
  }
199
205
  export const Audio = Object.freeze({
200
206
  getRecordingPermissionStatus,
201
207
  requestRecordingPermission,
202
- play: async (uri: string): Promise<AudioPlaybackStatus> => {
203
- if (typeof window === 'undefined') return unavailable.play(uri)
208
+ play: async (uri: string, options?: AudioPlayOptions): Promise<AudioPlaybackStatus> => {
209
+ if (typeof window === 'undefined') return unavailable.play(uri, options)
204
210
  if (openingRecording || recording)
205
211
  throw new Error('Audio.play: stop the current recording first')
206
212
  const url = new URL(uri, document.baseURI)
207
213
  if (!['https:', 'http:', 'blob:', 'data:'].includes(url.protocol))
208
214
  throw new Error('Audio.play: invalid browser audio URI')
215
+ const prepare = options?.positionMs !== undefined || options?.paused !== undefined
216
+ const positionMs = options?.positionMs ?? 0
217
+ if (typeof positionMs !== 'number' || !Number.isFinite(positionMs) || positionMs < 0)
218
+ throw new Error('Audio.play: position must be finite and nonnegative')
209
219
  clearPlayer()
210
220
  const audio = new window.Audio(url.href)
211
221
  player = audio
212
- for (const event of ['play', 'pause', 'ended'])
213
- audio.addEventListener(event, syncNowPlaying)
214
222
  audio.addEventListener('error', () => {
215
223
  if (player === audio)
216
224
  playbackError = audio.error?.message ?? 'Audio.play: decoding failed'
217
225
  })
226
+ if (prepare) {
227
+ const load: NonNullable<typeof pendingLoad> = { audio, paused: options?.paused ?? false }
228
+ pendingLoad = load
229
+ audio.preload = 'auto'
230
+ try {
231
+ await prepareAt(load, positionMs)
232
+ } catch (error) {
233
+ if (pendingLoad === load) {
234
+ pendingLoad = undefined
235
+ playbackError = (error as Error).message
236
+ }
237
+ throw error
238
+ }
239
+ if (player !== audio || pendingLoad !== load)
240
+ throw new Error('Audio.play: playback was replaced')
241
+ pendingLoad = undefined
242
+ if (load.paused) return playbackStatus()
243
+ }
218
244
  try {
219
245
  await audio.play()
220
246
  } catch (error) {
@@ -228,6 +254,10 @@ export const Audio = Object.freeze({
228
254
  pause: async (): Promise<AudioPlaybackStatus> => {
229
255
  if (typeof window === 'undefined') return unavailable.pause()
230
256
  const audio = requirePlayer()
257
+ if (pendingLoad) {
258
+ pendingLoad.paused = true
259
+ return playbackStatus()
260
+ }
231
261
  if (!audio.paused)
232
262
  await new Promise<void>((resolve) => {
233
263
  audio.addEventListener('pause', () => resolve(), { once: true })
@@ -238,6 +268,11 @@ export const Audio = Object.freeze({
238
268
  resume: async (): Promise<AudioPlaybackStatus> => {
239
269
  if (typeof window === 'undefined') return unavailable.resume()
240
270
  const audio = requirePlayer()
271
+ // a prepared load plays itself once its seek lands.
272
+ if (pendingLoad) {
273
+ pendingLoad.paused = false
274
+ return playbackStatus()
275
+ }
241
276
  if (audio.ended) audio.currentTime = 0
242
277
  await audio.play()
243
278
  if (player !== audio) throw new Error('Audio.resume: playback was replaced')
@@ -248,7 +283,8 @@ export const Audio = Object.freeze({
248
283
  if (!Number.isFinite(positionMs) || positionMs < 0)
249
284
  throw new Error('Audio.seek: position must be finite and nonnegative')
250
285
  const audio = requirePlayer()
251
- if (audio.readyState < 1) throw new Error('Audio.seek: playback is not ready')
286
+ if (audio.readyState < 1 || pendingLoad)
287
+ throw new Error('Audio.seek: playback is not ready')
252
288
  const seconds = Math.min(
253
289
  positionMs / 1000,
254
290
  Number.isFinite(audio.duration) ? audio.duration : Infinity
@@ -402,27 +438,23 @@ export const Audio = Object.freeze({
402
438
  watchInterruptions: unavailable.watchInterruptions,
403
439
  setNowPlayingInfo: async (info: AudioNowPlayingInfo): Promise<void> => {
404
440
  if (typeof window === 'undefined') return unavailable.setNowPlayingInfo(info)
405
- if (!info.title.trim())
406
- throw new Error('Audio.setNowPlayingInfo: title must not be empty')
407
441
  const audio = requirePlayer()
408
- if (info.artworkUri) {
409
- const artwork = new window.Image()
410
- artwork.src = info.artworkUri
411
- await artwork.decode()
412
- if (player !== audio) throw new Error('Audio.setNowPlayingInfo: playback was replaced')
413
- }
414
- mediaSession().metadata = new MediaMetadata({
415
- title: info.title,
416
- artist: info.artist,
417
- album: info.albumTitle,
418
- artwork: info.artworkUri ? [{ src: info.artworkUri }] : [],
419
- })
420
- installRemote()
421
- syncNowPlaying()
442
+ await claimNowPlaying(
443
+ {
444
+ key: 'global',
445
+ element: audio,
446
+ play: Audio.resume,
447
+ pause: Audio.pause,
448
+ seek: Audio.seek,
449
+ },
450
+ info,
451
+ 'Audio.setNowPlayingInfo',
452
+ () => player === audio
453
+ )
422
454
  },
423
455
  clearNowPlayingInfo: async (): Promise<void> => {
424
456
  if (typeof window === 'undefined') return unavailable.clearNowPlayingInfo()
425
- clearNowPlaying()
457
+ releaseNowPlaying('global')
426
458
  },
427
459
  watchRemoteCommands: (
428
460
  onEvent: (event: AudioRemoteCommandEvent) => void
@@ -430,11 +462,7 @@ export const Audio = Object.freeze({
430
462
  validateCallback(onEvent, 'Audio.watchRemoteCommands: onEvent must be a function')
431
463
  if (typeof window === 'undefined' || !navigator.mediaSession)
432
464
  return unavailable.watchRemoteCommands(onEvent)
433
- remoteListeners.add(onEvent)
434
- return () => {
435
- remoteListeners.delete(onEvent)
436
-
437
- }
465
+ return addRemoteListener(onEvent)
438
466
  },
439
467
  AudioPlayer,
440
468
  createPlayer,