Skip to content

Commit 4483afd

Browse files
Add ActorOnWebWorker example
1 parent 15af3c8 commit 4483afd

File tree

6 files changed

+352
-0
lines changed

6 files changed

+352
-0
lines changed
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "Example",
7+
platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
8+
dependencies: [
9+
.package(path: "../../"),
10+
],
11+
targets: [
12+
.executableTarget(
13+
name: "MyApp",
14+
dependencies: [
15+
.product(name: "JavaScriptKit", package: "JavaScriptKit"),
16+
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
17+
]
18+
),
19+
]
20+
)

Examples/ActorOnWebWorker/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# WebWorker + Actor example
2+
3+
Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands:
4+
5+
```sh
6+
$ (
7+
set -eo pipefail; \
8+
V="$(swiftc --version | head -n1)"; \
9+
TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
10+
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
11+
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
12+
)
13+
$ export SWIFT_SDK_ID=$(
14+
V="$(swiftc --version | head -n1)"; \
15+
TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
16+
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
17+
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
18+
)
19+
$ ./build.sh
20+
$ npx serve
21+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import JavaScriptEventLoop
2+
import JavaScriptKit
3+
4+
// Simple full-text search service
5+
actor SearchService {
6+
struct Error: Swift.Error, CustomStringConvertible {
7+
let message: String
8+
9+
var description: String {
10+
return self.message
11+
}
12+
}
13+
14+
let serialExecutor: any SerialExecutor
15+
16+
// Simple in-memory index: word -> positions
17+
var index: [String: [Int]] = [:]
18+
var originalContent: String = ""
19+
lazy var console: JSValue = {
20+
JSObject.global.console
21+
}()
22+
23+
nonisolated var unownedExecutor: UnownedSerialExecutor {
24+
return self.serialExecutor.asUnownedSerialExecutor()
25+
}
26+
27+
init(serialExecutor: any SerialExecutor) {
28+
self.serialExecutor = serialExecutor
29+
}
30+
31+
// Utility function for fetch
32+
func fetch(_ url: String) -> JSPromise {
33+
let jsFetch = JSObject.global.fetch.function!
34+
return JSPromise(jsFetch(url).object!)!
35+
}
36+
37+
func fetchAndIndex(url: String) async throws {
38+
let response = try await fetch(url).value()
39+
if response.status != 200 {
40+
throw Error(message: "Failed to fetch content")
41+
}
42+
let text = try await JSPromise(response.text().object!)!.value()
43+
let content = text.string!
44+
index(content)
45+
}
46+
47+
func index(_ contents: String) {
48+
self.originalContent = contents
49+
self.index = [:]
50+
51+
// Simple tokenization and indexing
52+
var position = 0
53+
let words = contents.lowercased().split(whereSeparator: { !$0.isLetter && !$0.isNumber })
54+
55+
for word in words {
56+
let wordStr = String(word)
57+
if wordStr.count > 1 { // Skip single-character words
58+
if index[wordStr] == nil {
59+
index[wordStr] = []
60+
}
61+
index[wordStr]?.append(position)
62+
}
63+
position += 1
64+
}
65+
66+
_ = console.log("Indexing complete with", index.count, "unique words")
67+
}
68+
69+
func search(_ query: String) -> [SearchResult] {
70+
let queryWords = query.lowercased().split(whereSeparator: { !$0.isLetter && !$0.isNumber })
71+
72+
if queryWords.isEmpty {
73+
return []
74+
}
75+
76+
var results: [SearchResult] = []
77+
78+
// Start with the positions of the first query word
79+
guard let firstWord = queryWords.first,
80+
let firstWordPositions = index[String(firstWord)]
81+
else {
82+
return []
83+
}
84+
85+
for position in firstWordPositions {
86+
// Extract context around this position
87+
let words = originalContent.lowercased().split(whereSeparator: {
88+
!$0.isLetter && !$0.isNumber
89+
})
90+
var contextWords: [String] = []
91+
92+
// Get words for context (5 words before, 10 words after)
93+
let contextStart = max(0, position - 5)
94+
let contextEnd = min(position + 10, words.count - 1)
95+
96+
if contextStart <= contextEnd && contextStart < words.count {
97+
for i in contextStart...contextEnd {
98+
if i < words.count {
99+
contextWords.append(String(words[i]))
100+
}
101+
}
102+
}
103+
104+
let context = contextWords.joined(separator: " ")
105+
results.append(SearchResult(position: position, context: context))
106+
}
107+
108+
return results
109+
}
110+
}
111+
112+
struct SearchResult {
113+
let position: Int
114+
let context: String
115+
}
116+
117+
@MainActor
118+
final class App {
119+
// DOM elements
120+
private let document = JSObject.global.document
121+
private let alert = JSObject.global.alert.function!
122+
123+
// UI elements
124+
private var container: JSValue
125+
private var urlInput: JSValue
126+
private var indexButton: JSValue
127+
private var searchInput: JSValue
128+
private var searchButton: JSValue
129+
private var statusElement: JSValue
130+
private var resultsElement: JSValue
131+
132+
// Search service
133+
private let service: SearchService
134+
135+
init(service: SearchService) {
136+
self.service = service
137+
container = document.getElementById("container")
138+
urlInput = document.getElementById("urlInput")
139+
indexButton = document.getElementById("indexButton")
140+
searchInput = document.getElementById("searchInput")
141+
searchButton = document.getElementById("searchButton")
142+
statusElement = document.getElementById("status")
143+
resultsElement = document.getElementById("results")
144+
setupEventHandlers()
145+
}
146+
147+
private func setupEventHandlers() {
148+
indexButton.onclick = .object(JSClosure { [weak self] _ in
149+
guard let self else { return .undefined }
150+
self.performIndex()
151+
return .undefined
152+
})
153+
154+
searchButton.onclick = .object(JSClosure { [weak self] _ in
155+
guard let self else { return .undefined }
156+
self.performSearch()
157+
return .undefined
158+
})
159+
}
160+
161+
private func performIndex() {
162+
let url = urlInput.value.string!
163+
164+
if url.isEmpty {
165+
alert("Please enter a URL")
166+
return
167+
}
168+
169+
updateStatus("Downloading and indexing content...")
170+
171+
Task { [weak self] in
172+
guard let self else { return }
173+
do {
174+
try await self.service.fetchAndIndex(url: url)
175+
await MainActor.run {
176+
self.updateStatus("Indexing complete!")
177+
}
178+
} catch {
179+
await MainActor.run {
180+
self.updateStatus("Error: \(error)")
181+
}
182+
}
183+
}
184+
}
185+
186+
private func performSearch() {
187+
let query = searchInput.value.string!
188+
189+
if query.isEmpty {
190+
alert("Please enter a search query")
191+
return
192+
}
193+
194+
updateStatus("Searching...")
195+
196+
Task { [weak self] in
197+
guard let self else { return }
198+
let searchResults = await self.service.search(query)
199+
await MainActor.run {
200+
self.displaySearchResults(searchResults)
201+
}
202+
}
203+
}
204+
205+
private func updateStatus(_ message: String) {
206+
statusElement.innerText = .string(message)
207+
}
208+
209+
private func displaySearchResults(_ results: [SearchResult]) {
210+
statusElement.innerText = .string("Search complete! Found \(results.count) results.")
211+
resultsElement.innerHTML = .string("")
212+
213+
if results.isEmpty {
214+
var noResults = document.createElement("p")
215+
noResults.innerText = .string("No results found.")
216+
_ = resultsElement.appendChild(noResults)
217+
} else {
218+
// Display up to 10 results
219+
for (index, result) in results.prefix(10).enumerated() {
220+
var resultItem = document.createElement("div")
221+
resultItem.style = .string(
222+
"padding: 10px; margin: 5px 0; background: #f5f5f5; border-left: 3px solid blue;"
223+
)
224+
resultItem.innerHTML = .string(
225+
"<strong>Result \(index + 1):</strong> \(result.context)")
226+
_ = resultsElement.appendChild(resultItem)
227+
}
228+
}
229+
}
230+
}
231+
232+
@main struct Main {
233+
@MainActor static var app: App?
234+
235+
static func main() {
236+
JavaScriptEventLoop.installGlobalExecutor()
237+
WebWorkerTaskExecutor.installGlobalExecutor()
238+
239+
Task {
240+
// Create dedicated worker and search service
241+
let dedicatedWorker = try await WebWorkerDedicatedExecutor()
242+
let service = SearchService(serialExecutor: dedicatedWorker)
243+
app = App(service: service)
244+
}
245+
}
246+
}
247+
248+
#if canImport(wasi_pthread)
249+
import wasi_pthread
250+
import WASILibc
251+
252+
/// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
253+
/// the Swift concurrency runtime.
254+
@_cdecl("pthread_mutex_lock")
255+
func pthread_mutex_lock(_ mutex: UnsafeMutablePointer<pthread_mutex_t>) -> Int32 {
256+
// DO NOT BLOCK MAIN THREAD
257+
var ret: Int32
258+
repeat {
259+
ret = pthread_mutex_trylock(mutex)
260+
} while ret == EBUSY
261+
return ret
262+
}
263+
#endif

Examples/ActorOnWebWorker/build.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
swift package --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -c release \
2+
plugin --allow-writing-to-package-directory \
3+
js --use-cdn --output ./Bundle

Examples/ActorOnWebWorker/index.html

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<html>
2+
3+
<head>
4+
<meta charset="utf-8">
5+
<title>WebWorker + Actor example</title>
6+
</head>
7+
8+
<body>
9+
<script type="module">
10+
import { init } from "./Bundle/index.js"
11+
init(fetch(new URL("./Bundle/main.wasm", import.meta.url)));
12+
</script>
13+
<h1>Full-text Search with Actor on Web Worker</h1>
14+
15+
<div id="container">
16+
<input type="text" id="urlInput"
17+
value="https://raw.githubusercontent.com/swiftlang/swift/refs/tags/swift-DEVELOPMENT-SNAPSHOT-2025-03-13-a/docs/SIL/Instructions.md"
18+
placeholder="Enter URL with text to index"
19+
style="width: 300px; padding: 8px; margin-right: 10px;">
20+
<button id="indexButton" style="padding: 8px 15px; margin-right: 10px;">Download & Index</button>
21+
<br>
22+
<br>
23+
<input type="text" id="searchInput" placeholder="Enter search query"
24+
style="width: 300px; padding: 8px; margin-right: 10px;">
25+
<button id="searchButton" style="padding: 8px 15px;">Search</button>
26+
<p id="status">Ready</p>
27+
<div id="results"></div>
28+
</div>
29+
</body>
30+
31+
</html>

Examples/ActorOnWebWorker/serve.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"headers": [{
3+
"source": "**/*",
4+
"headers": [
5+
{
6+
"key": "Cross-Origin-Embedder-Policy",
7+
"value": "require-corp"
8+
}, {
9+
"key": "Cross-Origin-Opener-Policy",
10+
"value": "same-origin"
11+
}
12+
]
13+
}]
14+
}

0 commit comments

Comments
 (0)