Skip to content

Commit 570928f

Browse files
committed
feat(spannerlib-node): add class-level documentation and TODOs for proto resolution
1 parent 926abbe commit 570928f

9 files changed

Lines changed: 108 additions & 6 deletions

File tree

spannerlib/wrappers/spannerlib-node/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,9 @@ async function run() {
4444

4545
The wrapper consists of:
4646
1. **`src/cpp/addon.cc`**: C++ Node-API bridge that handles thread boundaries and type conversions between V8 and C.
47-
2. **`src/ffi/utils.js`**: Helper functions to invoke native methods asynchronously using Promises.
47+
2. **`src/ffi/utils.ts`**: Helper functions to invoke native methods asynchronously using Promises.
4848
3. **`src/lib/`**: JavaScript classes (`Pool`, `Connection`, `Rows`) that provide a clean object-oriented interface.
49+
50+
### Component Interaction & Memory Management
51+
52+
When a JavaScript object (like a `Pool` or `Connection`) is created, it holds an ID referencing a pinned Go object in memory. The `spannerLib` singleton maintains a **`FinalizationRegistry`**. This registry allows Node.js to listen for when the JavaScript object is garbage collected. When GC occurs, the registry automatically triggers a cleanup call to the native layer to release the corresponding Go object, preventing native memory leaks even if the developer forgets to call `.close()` explicitly.

spannerlib/wrappers/spannerlib-node/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@
3636
"build/Release/*.node"
3737
],
3838
"dependencies": {
39-
"node-addon-api": "^8.0.0"
39+
"node-addon-api": "^8.0.0",
40+
"bindings": "^1.5.0",
41+
"@google-cloud/spanner": "^7.13.0"
4042
},
4143
"devDependencies": {
4244
"mocha": "^10.2.0",
4345
"typescript": "^5.4.0",
4446
"@types/node": "^20.11.0",
4547
"@types/mocha": "^10.0.6",
46-
"@google-cloud/spanner": "^7.13.0",
4748
"@babel/core": "^7.24.0",
4849
"@babel/cli": "^7.23.9",
4950
"sinon": "^18.0.0",

spannerlib/wrappers/spannerlib-node/scripts/fix-extensions.cjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
115
const fs = require('fs');
216
const path = require('path');
317

spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
#include <iostream>
1717
#include "libspanner.h"
1818

19+
// Documentation for Go function return fields (r0 - r4):
20+
// r0: Pinner ID (used for memory management to keep Go objects pinned)
21+
// r1: Error Code (0 for success, non-zero for error)
22+
// r2: Object ID (Handle to the created object, e.g., Pool or Connection)
23+
// r3: Message Length (Length of the protobuf message or error string in r4)
24+
// r4: Message Data (Pointer to protobuf bytes or JSON error message)
25+
1926
//
2027
// Worker 1: CreatePool asynchronously
2128
//

spannerlib/wrappers/spannerlib-node/src/ffi/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import { createRequire } from 'module';
1616
// @ts-ignore
1717
const _require = typeof require !== 'undefined' ? require : createRequire(import.meta.url);
18-
const addon = _require('../../../Release/spanner_napi.node');
18+
const addon = _require('bindings')('spanner_napi');
1919

2020
export const ENCODING_JSON = 0;
2121
export const ENCODING_PROTOBUF = 1;

spannerlib/wrappers/spannerlib-node/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import { Connection } from './lib/connection.js';
1818
import { Rows } from './lib/rows.js';
1919
import { SpannerLibError } from './ffi/utils.js';
2020

21+
/**
22+
* Releases all pinned resources and handles managed by the library.
23+
* This method should be called when shutting down the application or when the wrapper is no longer needed to prevent native memory leaks.
24+
*/
2125
export function cleanup(): void {
2226
spannerLib.releaseAll();
2327
}

spannerlib/wrappers/spannerlib-node/src/lib/connection.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,30 @@ import { Rows } from './rows.js';
1919
import { createRequire } from 'module';
2020
// @ts-ignore
2121
const _require = typeof require !== 'undefined' ? require : createRequire(import.meta.url);
22+
// TODO: Avoid tight coupling to internal paths of full client libraries.
23+
// Unlike other languages like Java, Python , Node client does not export its protos.
24+
// We need to explore how to import protos in Node
2225
const { google } = _require('@google-cloud/spanner/build/protos/protos.js');
2326

27+
/**
28+
* Manages a connection to the Spanner database.
29+
*
30+
* This class wraps the connection handle from the underlying Go library,
31+
* providing methods to execute SQL statements and manage transactions.
32+
*/
2433
export class Connection {
2534
public pool: Pool | null;
2635
public oid: number | null;
2736
public pinnerId: number | null;
2837
public closed: boolean;
2938

39+
/**
40+
* Creates a new connection within the specified pool.
41+
*
42+
* @param pool The pool to create the connection in.
43+
* @returns A Promise that resolves to a new Connection instance.
44+
* @throws {SpannerLibError} If creation fails in the Go library.
45+
*/
3046
static async create(pool: Pool): Promise<Connection> {
3147
const c = new Connection();
3248
c.pool = pool;
@@ -50,6 +66,14 @@ export class Connection {
5066
this.closed = false;
5167
}
5268

69+
/**
70+
* Executes a SQL statement on this connection.
71+
*
72+
* @param sqlString The SQL query string to execute.
73+
* @returns A Promise that resolves to a Rows instance containing results.
74+
* @throws {Error} If the connection is closed or not bound to a pool.
75+
* @throws {SpannerLibError} If execution fails in the Go library.
76+
*/
5377
async executeSql(sqlString: string): Promise<Rows> {
5478
if (this.closed) throw new Error("Connection is already closed");
5579
if (!this.pool) throw new Error("Connection is not bound to a Pool");
@@ -71,6 +95,11 @@ export class Connection {
7195
return new Rows(this, rowsId);
7296
}
7397

98+
/**
99+
* Closes the connection and releases associated resources.
100+
*
101+
* @returns A Promise that resolves when the connection is closed.
102+
*/
74103
async close(): Promise<void> {
75104
if (!this.closed) {
76105
this.closed = true;

spannerlib/wrappers/spannerlib-node/src/lib/pool.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,26 @@ import { ffi } from '../ffi/utils.js';
1616
import { spannerLib } from './spannerlib.js';
1717
import { Connection } from './connection.js';
1818

19+
/**
20+
* Manages a pool of database connections to Spanner.
21+
*
22+
* This class wraps the connection pool handle from the underlying Go library,
23+
* providing methods to create connections and manage the pool lifecycle.
24+
*/
1925
export class Pool {
2026
public oid: number | null;
2127
public pinnerId: number | null;
2228
public closed: boolean;
2329
public userAgent: string;
2430
public connStr: string;
2531

32+
/**
33+
* Creates a new connection pool.
34+
*
35+
* @param connectionString The connection string for the database.
36+
* @returns A Promise that resolves to a new Pool instance.
37+
* @throws {SpannerLibError} If creation fails in the Go library.
38+
*/
2639
static async create(connectionString: string): Promise<Pool> {
2740
// Detect if running in ESM context
2841
const isESM = typeof require === 'undefined';
@@ -50,11 +63,22 @@ export class Pool {
5063
this.connStr = connectionString;
5164
}
5265

66+
/**
67+
* Creates a new connection from the pool.
68+
*
69+
* @returns A Promise that resolves to a new Connection instance.
70+
* @throws {Error} If the pool is closed.
71+
*/
5372
async createConnection(): Promise<Connection> {
5473
if (this.closed) throw new Error("Pool is already closed");
5574
return await Connection.create(this);
5675
}
5776

77+
/**
78+
* Closes the pool and releases associated resources.
79+
*
80+
* @returns A Promise that resolves when the pool is closed.
81+
*/
5882
async close(): Promise<void> {
5983
if (!this.closed) {
6084
this.closed = true;

spannerlib/wrappers/spannerlib-node/src/lib/rows.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,18 @@ import { Connection } from './connection.js';
1818
import { createRequire } from 'module';
1919
// @ts-ignore
2020
const _require = typeof require !== 'undefined' ? require : createRequire(import.meta.url);
21+
// TODO: Avoid tight coupling to internal paths of full client libraries.
22+
// Unlike other languages like Java, Python , Node client does not export its protos.
23+
// We need to explore how to import protos in Node
2124
const { google } = _require('@google-cloud/spanner/build/protos/protos.js');
2225
const ListValue = google.protobuf.ListValue;
2326

24-
25-
27+
/**
28+
* An iterator over results returned by a SQL query.
29+
*
30+
* This class wraps the rows handle from the underlying Go library,
31+
* providing methods to fetch rows one by one.
32+
*/
2633
export class Rows {
2734
public connection: Connection;
2835
public oid: number;
@@ -35,6 +42,13 @@ export class Rows {
3542
this.closed = false;
3643
}
3744

45+
/**
46+
* Fetches the next row of data.
47+
*
48+
* @returns A Promise that resolves to a ListValue containing the row data, or null if there are no more rows.
49+
* @throws {Error} If the rows are already closed.
50+
* @throws {SpannerLibError} If fetching fails in the Go library.
51+
*/
3852
async next(): Promise<any> {
3953
if (this.closed) throw new Error("Rows are already closed");
4054

@@ -56,6 +70,11 @@ export class Rows {
5670
return ListValue.decode(handled.protobufBytes);
5771
}
5872

73+
/**
74+
* Closes the rows iterator and releases associated resources.
75+
*
76+
* @returns A Promise that resolves when the rows are closed.
77+
*/
5978
async close(): Promise<void> {
6079
if (!this.closed) {
6180
this.closed = true;

0 commit comments

Comments
 (0)