Skip to content

Commit 96fa0d2

Browse files
authored
Merge pull request #4067 from fbbdev/v3-alpha-feature/service-api
[v3] Built-in service standardisation and enhancement
2 parents d130544 + f87dce1 commit 96fa0d2

File tree

25 files changed

+1517
-367
lines changed

25 files changed

+1517
-367
lines changed

docs/src/content/docs/changelog.mdx

+14
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6464
- Add cancellable promise wrapper that propagates cancellation requests through promise chains by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100)
6565
- Add the ability to tie binding call cancellation to an `AbortSignal` by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100)
6666
- Support `data-wml-*` attributes for WML alongside the usual `wml-*` attributes by [@leaanthony](https://github.com/leaanthony)
67+
- Add `Configure` method on all services for late configuration/dynamic reconfiguration by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
68+
- `fileserver` service sends a 503 Service Unavailable response when unconfigured by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
69+
- `kvstore` service provides an in-memory key-value store by default when unconfigured by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
70+
- Add `Load` method on `kvstore` service to reload data from file after config changes by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
71+
- Add `Clear` method on `kvstore` service to delete all keys by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
72+
- Add type `Level` in `log` service to provide JS-side log-level constants by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
73+
- Add `Log` method on `log` service to specify log-level dynamically by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
74+
- `sqlite` service provides an in-memory DB by default when unconfigured by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
75+
- Add method `Close` on `sqlite` service to close the DB manually by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
76+
- Add cancellation support for query methods on `sqlite` service by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
77+
- Add prepared statement support to `sqlite` service with JS bindings by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
6778

6879
### Fixed
6980

@@ -121,6 +132,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
121132
- The runtime initialises as soon as it is imported, no need to wait for the window to load by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100)
122133
- The runtime does not export an init method anymore. A side effects import can be used to initialise it by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100)
123134
- Bound methods now return a `CancellablePromise` that rejects with a `CancelError` if cancelled. The actual result of the call is discarded by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100)
135+
- Built-in service types are now consistently called `Service` by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
136+
- Built-in service creation functions with options are now consistently called `NewWithConfig` by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
137+
- `Select` method on `sqlite` service is now named `Query` for consistency with Go APIs by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
124138

125139
## v3.0.0-alpha.9 - 2025-01-13
126140

docs/src/content/docs/learn/services.mdx

+6-9
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,16 @@ application.NewServiceWithOptions(fileserver.New(&fileserver.Config{
159159
Let's look at a simplified version of the `fileserver` service as an example:
160160

161161
```go
162+
type Config struct {
163+
RootPath string
164+
}
165+
162166
type Service struct {
163-
config *Config
164-
fs http.Handler
167+
fs http.Handler
165168
}
166169

167-
func New(config *Config) *Service {
170+
func NewWithConfig(config *Config) *Service {
168171
return &Service{
169-
config: config,
170172
fs: http.FileServer(http.Dir(config.RootPath)),
171173
}
172174
}
@@ -175,11 +177,6 @@ func (s *Service) ServiceName() string {
175177
return "github.com/wailsapp/wails/v3/services/fileserver"
176178
}
177179

178-
func (s *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
179-
// Any initialization code here
180-
return nil
181-
}
182-
183180
func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
184181
s.fs.ServeHTTP(w, r)
185182
}

docs/src/content/docs/tutorials/01-creating-a-service.mdx

+199-24
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ This will show you how to organize your code into reusable services and handle e
4040
return &QRService{}
4141
}
4242

43-
// GenerateQRCode creates a QR code from the given text
44-
func (s *QRService) GenerateQRCode(text string, size int) ([]byte, error) {
43+
// Generate creates a QR code from the given text
44+
func (s *QRService) Generate(text string, size int) ([]byte, error) {
4545
// Generate the QR code
4646
qr, err := qrcode.New(text, qrcode.Medium)
4747
if err != nil {
@@ -149,21 +149,21 @@ This will show you how to organize your code into reusable services and handle e
149149
// This file is automatically generated. DO NOT EDIT
150150

151151
/**
152-
* QRService handles QR code generation
153-
* @module
154-
*/
152+
* QRService handles QR code generation
153+
* @module
154+
*/
155155

156156
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
157157
// @ts-ignore: Unused imports
158158
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
159159

160160
/**
161-
* GenerateQRCode creates a QR code from the given text
162-
* @param {string} text
163-
* @param {number} size
164-
* @returns {Promise<string> & { cancel(): void }}
165-
*/
166-
export function GenerateQRCode(text, size) {
161+
* Generate creates a QR code from the given text
162+
* @param {string} text
163+
* @param {number} size
164+
* @returns {Promise<string> & { cancel(): void }}
165+
*/
166+
export function Generate(text, size) {
167167
let $resultPromise = /** @type {any} */($Call.ByID(3576998831, text, size));
168168
let $typingPromise = /** @type {any} */($resultPromise.then(($result) => {
169169
return $Create.ByteSlice($result);
@@ -173,7 +173,7 @@ This will show you how to organize your code into reusable services and handle e
173173
}
174174
```
175175

176-
We can see that the bindings are generated for the `GenerateQRCode` method. The parameter names have been preserved,
176+
We can see that the bindings are generated for the `Generate` method. The parameter names have been preserved,
177177
as well as the comments. JSDoc has also been generated for the method to provide type information to your IDE.
178178

179179
:::note
@@ -190,14 +190,36 @@ This will show you how to organize your code into reusable services and handle e
190190
The bindings generator also supports generating Typescript bindings. You can do this by running `wails3 generate bindings -ts`.
191191
:::
192192

193-
<br/>
193+
The generated service is re-exported by an `index.js` file:
194+
195+
```js title="bindings/changeme/index.js"
196+
// @ts-check
197+
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
198+
// This file is automatically generated. DO NOT EDIT
199+
200+
import * as QRService from "./qrservice.js";
201+
export {
202+
QRService
203+
};
204+
```
205+
206+
You may then access it through the simplified import path
207+
`./bindings/changeme` consisting just of your Go package path,
208+
without specifying any file name.
209+
210+
:::note
211+
Simplified import paths are only available when using frontend bundlers.
212+
If you prefer a vanilla frontend that does not employ a bundler,
213+
you will have to import either `index.js` or `qrservice.js` manually.
214+
:::
215+
<br/>
194216

195217
6. ## Use Bindings in Frontend
196218

197219
Firstly, update `frontend/src/main.js` to use the new bindings:
198220

199221
```js title="frontend/src/main.js"
200-
import { GenerateQRCode } from './bindings/changeme/qrservice.js';
222+
import { QRService } from './bindings/changeme';
201223

202224
async function generateQR() {
203225
const text = document.getElementById('text').value;
@@ -208,7 +230,7 @@ This will show you how to organize your code into reusable services and handle e
208230

209231
try {
210232
// Generate QR code as base64
211-
const qrCodeBase64 = await GenerateQRCode(text, 256);
233+
const qrCodeBase64 = await QRService.Generate(text, 256);
212234

213235
// Display the QR code
214236
const qrDiv = document.getElementById('qrcode');
@@ -285,7 +307,7 @@ This will show you how to organize your code into reusable services and handle e
285307

286308
Type in some text and click the "Generate QR Code" button. You should see a QR code in the center of the page:
287309

288-
<Image src={qr1} alt="QR Code"/>
310+
<Image src={qr1} alt="QR Code"/>
289311

290312
<br/>
291313
<br/>
@@ -303,13 +325,14 @@ This will show you how to organize your code into reusable services and handle e
303325
If your service defines Go's standard http handler function `ServeHTTP(w http.ResponseWriter, r *http.Request)`,
304326
then it can be made accessible on the frontend. Let's extend our QR code service to do this:
305327

306-
```go title="qrservice.go" ins={5-6,36-63}
328+
```go title="qrservice.go" ins={4-5,37-65}
307329
package main
308330

309331
import (
310-
"github.com/skip2/go-qrcode"
311332
"net/http"
312333
"strconv"
334+
335+
"github.com/skip2/go-qrcode"
313336
)
314337

315338
// QRService handles QR code generation
@@ -322,8 +345,8 @@ This will show you how to organize your code into reusable services and handle e
322345
return &QRService{}
323346
}
324347

325-
// GenerateQRCode creates a QR code from the given text
326-
func (s *QRService) GenerateQRCode(text string, size int) ([]byte, error) {
348+
// Generate creates a QR code from the given text
349+
func (s *QRService) Generate(text string, size int) ([]byte, error) {
327350
// Generate the QR code
328351
qr, err := qrcode.New(text, qrcode.Medium)
329352
if err != nil {
@@ -358,7 +381,7 @@ This will show you how to organize your code into reusable services and handle e
358381
}
359382

360383
// Generate the QR code
361-
qrCodeData, err := s.GenerateQRCode(text, size)
384+
qrCodeData, err := s.Generate(text, size)
362385
if err != nil {
363386
http.Error(w, err.Error(), http.StatusInternalServerError)
364387
return
@@ -408,11 +431,14 @@ This will show you how to organize your code into reusable services and handle e
408431
}
409432
```
410433

434+
:::note
435+
If you do not set the `Route` option explicitly,
436+
the HTTP handler won't be accessible from the frontend.
437+
:::
438+
411439
Finally, update `main.js` to make the image source the path to the QR code service, passing the text as a query parameter:
412440

413441
```js title="frontend/src/main.js"
414-
import { GenerateQRCode } from './bindings/changeme/qrservice.js';
415-
416442
async function generateQR() {
417443
const text = document.getElementById('text').value;
418444
if (!text) {
@@ -422,7 +448,7 @@ This will show you how to organize your code into reusable services and handle e
422448

423449
const img = document.getElementById('qrcode');
424450
// Make the image source the path to the QR code service, passing the text
425-
img.src = `/qrservice?text=${text}`
451+
img.src = `/qrservice?text=${encodeURIComponent(text)}`
426452
}
427453

428454
export function initializeQRGenerator() {
@@ -440,4 +466,153 @@ This will show you how to organize your code into reusable services and handle e
440466
<Image src={qr1} alt="QR Code"/>
441467
<br/>
442468

469+
8. ## Supporting dynamic configurations
470+
471+
In the example above we used a hardcoded route `/qrservice`.
472+
If you edit `main.go` and change the `Route` option without updating `main.js`,
473+
the application will break:
474+
475+
```go title="main.go" ins={3}
476+
// ...
477+
application.NewService(NewQRService(), application.ServiceOptions{
478+
Route: "/services/qr",
479+
}),
480+
// ...
481+
```
482+
483+
Hardcoded routes can be good for many applications,
484+
but if you need more flexibility, method bindings and HTTP handlers
485+
can work together to improve the development experience.
486+
487+
The `ServiceStartup` Lifecycle method provides access to service options at startup,
488+
and a custom method can be used to announce the configured route to the frontend.
489+
490+
First, implement the `ServiceStartup` interface and add a new `URL` method:
491+
492+
```go title="qrservice.go" ins={4,6,10,15,23-27,46-55}
493+
package main
494+
495+
import (
496+
"context"
497+
"net/http"
498+
"net/url"
499+
"strconv"
500+
501+
"github.com/skip2/go-qrcode"
502+
"github.com/wailsapp/wails/v3/pkg/application"
503+
)
504+
505+
// QRService handles QR code generation
506+
type QRService struct {
507+
route string
508+
}
509+
510+
// NewQRService creates a new QR service
511+
func NewQRService() *QRService {
512+
return &QRService{}
513+
}
514+
515+
// ServiceStartup runs at application startup.
516+
func (s *QRService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
517+
s.route = options.Route
518+
return nil
519+
}
520+
521+
// Generate creates a QR code from the given text
522+
func (s *QRService) Generate(text string, size int) ([]byte, error) {
523+
// Generate the QR code
524+
qr, err := qrcode.New(text, qrcode.Medium)
525+
if err != nil {
526+
return nil, err
527+
}
528+
529+
// Convert to PNG
530+
png, err := qr.PNG(size)
531+
if err != nil {
532+
return nil, err
533+
}
534+
535+
return png, nil
536+
}
537+
538+
// URL returns an URL that may be used to fetch
539+
// a QR code with the given text and size.
540+
// It returns an error if the HTTP handler is not available.
541+
func (s *QRService) URL(text string, size int) (string, error) {
542+
if s.route == "" {
543+
return "", errors.New("http handler unavailable")
544+
}
545+
546+
return fmt.Sprintf("%s?text=%s&size=%d", s.route, url.QueryEscape(text), size), nil
547+
}
548+
549+
func (s *QRService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
550+
// Extract the text parameter from the request
551+
text := r.URL.Query().Get("text")
552+
if text == "" {
553+
http.Error(w, "Missing 'text' parameter", http.StatusBadRequest)
554+
return
555+
}
556+
// Extract Size parameter from the request
557+
sizeText := r.URL.Query().Get("size")
558+
if sizeText == "" {
559+
sizeText = "256"
560+
}
561+
size, err := strconv.Atoi(sizeText)
562+
if err != nil {
563+
http.Error(w, "Invalid 'size' parameter", http.StatusBadRequest)
564+
return
565+
}
566+
567+
// Generate the QR code
568+
qrCodeData, err := s.Generate(text, size)
569+
if err != nil {
570+
http.Error(w, err.Error(), http.StatusInternalServerError)
571+
return
572+
}
573+
574+
// Write the QR code data to the response
575+
w.Header().Set("Content-Type", "image/png")
576+
w.Write(qrCodeData)
577+
}
578+
```
579+
580+
Now update `main.js` to use the `URL` method in place of a hardcoded path:
581+
582+
```js title="frontend/src/main.js" ins={1,11-12}
583+
import { QRService } from "./bindings/changeme";
584+
585+
async function generateQR() {
586+
const text = document.getElementById('text').value;
587+
if (!text) {
588+
alert('Please enter some text');
589+
return;
590+
}
591+
592+
const img = document.getElementById('qrcode');
593+
// Invoke the URL method to obtain an URL for the given text.
594+
img.src = await QRService.URL(text, 256);
595+
}
596+
597+
export function initializeQRGenerator() {
598+
const button = document.getElementById('generateButton');
599+
if (button) {
600+
button.addEventListener('click', generateQR);
601+
} else {
602+
console.error('Generate button not found');
603+
}
604+
}
605+
```
606+
607+
It should work just like the previous example,
608+
but changing the service route in `main.go`
609+
will not break the frontend anymore.
610+
611+
:::note
612+
If a Go method returns a non-nil error,
613+
the promise on the JS side will reject
614+
and await statements will throw an exception.
615+
:::
616+
<br/>
617+
443618
</Steps>

v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
33
// This file is automatically generated. DO NOT EDIT
44

5-
import * as KeyValueStore from "./keyvaluestore.js";
5+
import * as Service from "./service.js";
66
export {
7-
KeyValueStore
7+
Service
88
};

0 commit comments

Comments
 (0)