diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8566f6e5..1445f064 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: matrix: project: - helia-101 + - helia-angular - helia-browser-verified-fetch - helia-cjs - helia-electron @@ -90,6 +91,7 @@ jobs: matrix: project: - helia-101 + - helia-angular - helia-cjs - helia-browser-verified-fetch - helia-electron diff --git a/examples/helia-angular/.editorconfig b/examples/helia-angular/.editorconfig new file mode 100644 index 00000000..f166060d --- /dev/null +++ b/examples/helia-angular/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/examples/helia-angular/.gitignore b/examples/helia-angular/.gitignore new file mode 100644 index 00000000..cc7b1413 --- /dev/null +++ b/examples/helia-angular/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/helia-angular/README.md b/examples/helia-angular/README.md new file mode 100644 index 00000000..facaf1b5 --- /dev/null +++ b/examples/helia-angular/README.md @@ -0,0 +1,135 @@ +

+ + Helia logo + +

+ +

Using Helia inside an Angular.js app

+ +

+ +
+ Explore the docs + · + View Demo + · + Report Bug + · + Request Feature/Example +

+ +## Table of Contents + +See https://github.com/ipfs-examples/helia-examples#table-of-contents for more information about Helia examples. + +## About The Project + +- Read the [docs](https://ipfs.github.io/helia/modules/helia.html) +- Look into other [examples](https://github.com/ipfs-examples/helia-examples) to learn how to spawn a Helia node in Node.js and in the Browser +- Visit https://dweb-primer.ipfs.io to learn about IPFS and the concepts that underpin it +- Head over to https://proto.school to take interactive tutorials that cover core IPFS APIs +- Check out https://docs.ipfs.io for tips, how-tos and more +- See https://blog.ipfs.io for news and more +- Need help? Please ask 'How do I?' questions on https://discuss.ipfs.io + +## Getting Started + +### Prerequisites + +https://github.com/ipfs-examples/helia-examples#prerequisites + +### Installation and Running example + +```console +> npm install +> ng serve or npm start +``` + +Now open your browser at `http://localhost:4200/` + +## Available Scripts + +In the project directory, you can run: + +### Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +### Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +### Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +### Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +### Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +### Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. + + +## Usage + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.1.4. integrated with `helia`. + +You can start editing the page by modifying `src/app/app.component.html`. The page auto-updates as you edit the file. + +### Learn More + +To learn more about Angular.js, take a look at the following resources: + +- [Angular.js Documentation](https://angular.dev/overview) - learn about Angular.js features. +- [Learn Angular.js](https://angular.dev/tutorials/learn-angular) - an interactive Angular.js tutorial. + +You can check out [the Angular.js GitHub repository](https://github.com/angular) - your feedback and contributions are welcome! + +### Deploy on Vercel + +The easiest way to deploy your Angular.js app is to use the [Vercel Platform](https://vercel.com/solutions/angular#the-easiest-way-to-deploy). + +Check out our [Angular.js deployment documentation](https://angular.dev/tools/cli/deployment) for more details. + +_For more examples, please refer to the [Documentation](https://github.com/ipfs-examples/helia-examples#documentation)_ + +## Documentation + +- [IPFS Primer](https://dweb-primer.ipfs.io/) +- [IPFS Docs](https://docs.ipfs.io/) +- [Tutorials](https://proto.school) +- [More examples](https://github.com/ipfs-examples/helia-examples) +- [API - Helia](https://ipfs.github.io/helia/modules/helia.html) +- [API - @helia/unixfs](https://ipfs.github.io/helia-unixfs/modules/helia.html) + +## Contributing + +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +1. Fork the IPFS Project +2. Create your Feature Branch (`git checkout -b feature/amazing-feature`) +3. Commit your Changes (`git commit -a -m 'feat: add some amazing feature'`) +4. Push to the Branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## Want to hack on IPFS? + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) + +The IPFS implementation in JavaScript needs your help! There are a few things you can do right now to help out: + +Read the [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md) and [JavaScript Contributing Guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md). + +- **Check out existing issues** The [issue list](https://github.com/ipfs/helia/issues) has many that are marked as ['help wanted'](https://github.com/ipfs/helia/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22help+wanted%22) or ['difficulty:easy'](https://github.com/ipfs/helia/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Adifficulty%3Aeasy) which make great starting points for development, many of which can be tackled with no prior IPFS knowledge +- **Look at the [Helia Roadmap](https://github.com/ipfs/helia/blob/main/ROADMAP.md)** This are the high priority items being worked on right now +- **Perform code reviews** More eyes will help + a. speed the project along + b. ensure quality, and + c. reduce possible future bugs +- **Add tests**. There can never be enough tests \ No newline at end of file diff --git a/examples/helia-angular/angular.json b/examples/helia-angular/angular.json new file mode 100644 index 00000000..dbcf0899 --- /dev/null +++ b/examples/helia-angular/angular.json @@ -0,0 +1,104 @@ +{ + + "version": 1, + "newProjectRoot": "projects", + "projects": { + "helia-angular": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "allowedCommonJsDependencies": ["helia"], + "outputPath": "dist/helia-angular", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "helia-angular:build:production" + }, + "development": { + "buildTarget": "helia-angular:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "configurations": { + "ci": { + "browsers": ["ChromeHeadlessCI"], + "watch": false, + "progress": false + } + }, + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/examples/helia-angular/package.json b/examples/helia-angular/package.json new file mode 100644 index 00000000..c66f409c --- /dev/null +++ b/examples/helia-angular/package.json @@ -0,0 +1,57 @@ +{ + "name": "helia-angular", + "version": "0.0.0", + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "serve:ssr:helia-angular": "node dist/helia-angular/server/server.mjs" + }, + "private": true, + "dependencies": { + "@angular/animations": "^19.1.4", + "@angular/common": "^19.1.4", + "@angular/compiler": "^19.1.4", + "@angular/core": "^19.1.4", + "@angular/forms": "^19.1.4", + "@angular/platform-browser": "^19.1.4", + "@angular/platform-browser-dynamic": "^19.1.4", + "@angular/platform-server": "^19.1.4", + "@angular/router": "^19.1.4", + "@angular/ssr": "^19.1.5", + "@chainsafe/libp2p-noise": "^11.0.0", + "@chainsafe/libp2p-yamux": "^3.0.7", + "@helia/core": "^0.0.0", + "@helia/interface": "^5.2.0", + "@helia/unixfs": "^4.0.2", + "@libp2p/bootstrap": "^6.0.0", + "@libp2p/interface": "^2.4.0", + "@libp2p/websockets": "^9.1.2", + "buffer": "^6.0.3", + "helia": "^5.2.0", + "libp2p": "^2.5.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.1.4", + "@angular/cli": "^19.1.4", + "@angular/compiler-cli": "^19.1.4", + "@types/express": "^4.17.17", + "@types/jasmine": "~5.1.0", + "@types/node": "^18.18.0", + "jasmine-core": "~5.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "multiformats": "^13.3.1", + "patch-package": "^8.0.0", + "typescript": "~5.7.2" + } +} diff --git a/examples/helia-angular/public/favicon.ico b/examples/helia-angular/public/favicon.ico new file mode 100644 index 00000000..3580aaf4 Binary files /dev/null and b/examples/helia-angular/public/favicon.ico differ diff --git a/examples/helia-angular/src/app/app.component.css b/examples/helia-angular/src/app/app.component.css new file mode 100644 index 00000000..a998eedf --- /dev/null +++ b/examples/helia-angular/src/app/app.component.css @@ -0,0 +1,106 @@ +/* General styles for the app container */ +.App { + font-family: 'Arial', sans-serif; + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + background-color: #f9f9f9; + color: #333; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + max-width: 500px; + margin: 20px auto; + } + + h1 { + font-size: 24px; + margin-bottom: 20px; + color: #333; + text-align: center; + font-weight: bold; + background-color: #c20726; + padding: 10px; + border-radius: 4px; + } + + /* Status box */ + #heliaStatus { + width: 100%; + + text-align: center; + padding: 10px; + border-radius: 4px; + font-weight: bold; + margin-bottom: 20px; + color: #fff; + background-color: #4caf50; /* Default: green */ + transition: background-color 0.3s, border-color 0.3s; + } + + /* Input box styling */ + #textInput { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 16px; + margin-bottom: 15px; + outline: none; + transition: border-color 0.3s; + } + + #textInput:focus { + border-color: #007bff; /* Focus color: blue */ + } + + /* Buttons styling */ + button { + background-color: #007bff; + color: #fff; + border: none; + padding: 10px 15px; + font-size: 16px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s, transform 0.2s; + margin-bottom: 10px; + width: 100%; + } + + button:hover { + background-color: #0056b3; + transform: scale(1.02); + } + + button:active { + background-color: #003d80; + } + + /* Output containers */ + #cidOutput, + #committedTextOutput { + margin-top: 10px; + padding: 10px; + background-color: #f1f1f1; + border: 1px dashed #ccc; + border-radius: 4px; + width: 100%; + font-size: 14px; + text-align: center; + word-break: break-word; + } + + /* Responsive design */ + @media (max-width: 600px) { + .App { + padding: 15px; + } + + button { + font-size: 14px; + padding: 8px 12px; + } + } + + \ No newline at end of file diff --git a/examples/helia-angular/src/app/app.component.html b/examples/helia-angular/src/app/app.component.html new file mode 100644 index 00000000..aa4b3b5b --- /dev/null +++ b/examples/helia-angular/src/app/app.component.html @@ -0,0 +1,21 @@ +
+

Helia-Angular app is running!

+
+ {{ heliaStatus }} +
+ + +
textCid: {{ cidString }}
+
+ +
Committed Text: {{ committedText }}
+
+
+ {{ error }} +
+
diff --git a/examples/helia-angular/src/app/app.component.spec.ts b/examples/helia-angular/src/app/app.component.spec.ts new file mode 100644 index 00000000..e7d83db7 --- /dev/null +++ b/examples/helia-angular/src/app/app.component.spec.ts @@ -0,0 +1,29 @@ +import { TestBed } from '@angular/core/testing' +import { AppComponent } from './app.component' + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent] + }).compileComponents() + }) + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent) + const app = fixture.componentInstance + expect(app).toBeTruthy() + }) + + it('should have the \'Helia-Angular\' title', () => { + const fixture = TestBed.createComponent(AppComponent) + const app = fixture.componentInstance + expect(app.title).toEqual('Helia-Angular') + }) + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent) + fixture.detectChanges() + const compiled = fixture.nativeElement as HTMLElement + expect(compiled.querySelector('h1')?.textContent).toContain('Helia-Angular app is running!') + }) +}) diff --git a/examples/helia-angular/src/app/app.component.ts b/examples/helia-angular/src/app/app.component.ts new file mode 100644 index 00000000..80842250 --- /dev/null +++ b/examples/helia-angular/src/app/app.component.ts @@ -0,0 +1,64 @@ +import { isPlatformBrowser, CommonModule } from '@angular/common' +import { Component, PLATFORM_ID, Inject, OnInit, OnDestroy } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { Subscription } from 'rxjs' +import { CommitTextService } from './services/commit-text.service' +import { HeliaService } from './services/helia.service' + +@Component({ + selector: 'app-root', + standalone: true, + imports: [FormsModule, CommonModule], + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent implements OnInit, OnDestroy { + title = 'Helia-Angular' + text: string = '' + cidString: string = '' + committedText: string = '' + isBrowser: boolean + private statusSubscription?: Subscription + heliaStatus = '' + error: string = '' + + constructor ( + private readonly commitTextService: CommitTextService, + @Inject(PLATFORM_ID) platformId: Object, + private readonly heliaService: HeliaService + ) { + this.isBrowser = isPlatformBrowser(platformId) + } + + ngOnInit () { + this.statusSubscription = this.heliaService.status$.subscribe(status => { + this.heliaStatus = status.initialized + ? `Connected (${status.peerId})` + : status.error ? 'Error: ' + status.error.message : 'Initializing...' + }) + } + + ngOnDestroy () { + this.statusSubscription?.unsubscribe() + } + + async addTextToNode () { + if (!this.isBrowser) return + try { + this.error = '' + this.cidString = (await this.commitTextService.commitText(this.text)) || '' + } catch (err) { + this.error = err instanceof Error ? err.message : 'Unknown error occurred' + } + } + + async fetchCommittedText () { + if (!this.isBrowser) return + try { + this.error = '' + this.committedText = (await this.commitTextService.fetchCommittedText(this.cidString)) || '' + } catch (err) { + this.error = err instanceof Error ? err.message : 'Unknown error occurred' + } + } +} diff --git a/examples/helia-angular/src/app/app.config.server.ts b/examples/helia-angular/src/app/app.config.server.ts new file mode 100644 index 00000000..8489dfd8 --- /dev/null +++ b/examples/helia-angular/src/app/app.config.server.ts @@ -0,0 +1,14 @@ +import { mergeApplicationConfig, type ApplicationConfig } from '@angular/core' +import { provideServerRendering } from '@angular/platform-server' +import { provideServerRoutesConfig } from '@angular/ssr' +import { appConfig } from './app.config' +import { serverRoutes } from './app.routes.server' + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering(), + provideServerRoutesConfig(serverRoutes) + ] +} + +export const config = mergeApplicationConfig(appConfig, serverConfig) diff --git a/examples/helia-angular/src/app/app.config.ts b/examples/helia-angular/src/app/app.config.ts new file mode 100644 index 00000000..a7141bba --- /dev/null +++ b/examples/helia-angular/src/app/app.config.ts @@ -0,0 +1,7 @@ +import { type ApplicationConfig } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes)] +} diff --git a/examples/helia-angular/src/app/app.routes.server.ts b/examples/helia-angular/src/app/app.routes.server.ts new file mode 100644 index 00000000..ca39d33d --- /dev/null +++ b/examples/helia-angular/src/app/app.routes.server.ts @@ -0,0 +1,8 @@ +import { RenderMode, type ServerRoute } from '@angular/ssr' + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Prerender + } +] diff --git a/examples/helia-angular/src/app/app.routes.ts b/examples/helia-angular/src/app/app.routes.ts new file mode 100644 index 00000000..51abde6c --- /dev/null +++ b/examples/helia-angular/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import { type Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/helia-angular/src/app/services/commit-text.service.spec.ts b/examples/helia-angular/src/app/services/commit-text.service.spec.ts new file mode 100644 index 00000000..f246fc09 --- /dev/null +++ b/examples/helia-angular/src/app/services/commit-text.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed } from '@angular/core/testing' +import { CommitTextService } from './commit-text.service' + +describe('CommitTextService', () => { + let service: CommitTextService + + beforeEach(() => { + TestBed.configureTestingModule({}) + service = TestBed.inject(CommitTextService) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) +}) diff --git a/examples/helia-angular/src/app/services/commit-text.service.ts b/examples/helia-angular/src/app/services/commit-text.service.ts new file mode 100644 index 00000000..b7c09562 --- /dev/null +++ b/examples/helia-angular/src/app/services/commit-text.service.ts @@ -0,0 +1,61 @@ +import { isPlatformBrowser } from '@angular/common' +import { Injectable, PLATFORM_ID, Inject } from '@angular/core' +import { CID } from 'multiformats/cid' +import { HeliaService } from './helia.service' + +@Injectable({ + providedIn: 'root' +}) +export class CommitTextService { + constructor ( + private readonly heliaService: HeliaService, + @Inject(PLATFORM_ID) private readonly platformId: Object + ) {} + + async commitText (text: string): Promise { + if (!isPlatformBrowser(this.platformId)) { + return null + } + + try { + const fs = await this.heliaService.getFs() + const encoder = new TextEncoder() + const bytes = encoder.encode(text) + const cid = await fs.addBytes(bytes) + return cid.toString() + } catch (error) { + console.error('Error committing text:', error) + throw error // Re-throw to let component handle it + } + } + + async fetchCommittedText (cidString: string): Promise { + if (!isPlatformBrowser(this.platformId)) { + return null + } + + try { + const fs = await this.heliaService.getFs() + const decoder = new TextDecoder() + const chunks: Uint8Array[] = [] + + const cid = CID.parse(cidString) + + for await (const chunk of fs.cat(cid)) { + chunks.push(chunk) + } + + const allBytes = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)) + let offset = 0 + for (const chunk of chunks) { + allBytes.set(chunk, offset) + offset += chunk.length + } + + return decoder.decode(allBytes) + } catch (error) { + console.error('Error fetching committed text:', error) + throw error // Re-throw to let component handle it + } + } +} diff --git a/examples/helia-angular/src/app/services/helia.service.spec.ts b/examples/helia-angular/src/app/services/helia.service.spec.ts new file mode 100644 index 00000000..c74afc4b --- /dev/null +++ b/examples/helia-angular/src/app/services/helia.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed } from '@angular/core/testing' +import { HeliaService } from './helia.service' + +describe('HeliaService', () => { + let service: HeliaService + + beforeEach(() => { + TestBed.configureTestingModule({}) + service = TestBed.inject(HeliaService) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) +}) diff --git a/examples/helia-angular/src/app/services/helia.service.ts b/examples/helia-angular/src/app/services/helia.service.ts new file mode 100644 index 00000000..ecd931cc --- /dev/null +++ b/examples/helia-angular/src/app/services/helia.service.ts @@ -0,0 +1,123 @@ +import { isPlatformBrowser } from '@angular/common' +import { Injectable, PLATFORM_ID, Inject } from '@angular/core' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { unixfs } from '@helia/unixfs' +import { bootstrap } from '@libp2p/bootstrap' +import { webSockets } from '@libp2p/websockets' +import { createHelia } from 'helia' +import { createLibp2p } from 'libp2p' +import { BehaviorSubject, type Observable, firstValueFrom } from 'rxjs' +import { filter } from 'rxjs/operators' +import type { Helia } from '@helia/interface' +import type { UnixFS } from '@helia/unixfs' + +export interface HeliaStatus { + initialized: boolean + error?: Error + peerId?: string +} + +@Injectable({ + providedIn: 'root' +}) +export class HeliaService { + private helia: Helia | null = null + private fs: UnixFS | null = null + private readonly statusSubject = new BehaviorSubject({ initialized: false }) + private readonly initPromise: Promise | null = null + + // IPFS bootstrap nodes + private readonly bootstrapNodes = [ + '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb' + ] + + constructor (@Inject(PLATFORM_ID) private readonly platformId: Object) { + if (isPlatformBrowser(this.platformId)) { + this.initPromise = this.initHelia() + } + } + + get status$ (): Observable { + return this.statusSubject.asObservable() + } + + private async initHelia (): Promise { + if (this.helia) return + + try { + const libp2p = await createLibp2p({ + // addresses: { + // listen: ['/ip4/127.0.0.1/tcp/0/ws'] + + // }, + transports: [webSockets()], + connectionEncrypters: [(noise() as any)], + streamMuxers: [(_: any) => yamux()() as any], + + peerDiscovery: [ + bootstrap({ + list: this.bootstrapNodes + }) as any + ] + }) + + this.helia = await createHelia({ libp2p }) + this.fs = unixfs(this.helia) + + this.statusSubject.next({ + initialized: true, + peerId: (this.helia as any).libp2p.peerId.toString() + }) + } catch (error) { + console.error('Helia initialization error:', error) + this.statusSubject.next({ + initialized: false, + error: error instanceof Error ? error : new Error('Unknown error') + }) + throw error + } + } + + private async waitForInitialization (): Promise { + if (!isPlatformBrowser(this.platformId)) { + throw new Error('Helia is only available in browser environment') + } + + if (this.statusSubject.value.initialized) { + return + } + + await firstValueFrom( + this.status$.pipe( + filter(status => status.initialized || Boolean(status.error)) + ) + ) + + if (this.statusSubject.value.error) { + throw this.statusSubject.value.error + } + } + + async getHelia (): Promise { + await this.waitForInitialization() + + if (!this.helia) { + throw new Error('Helia failed to initialize') + } + + return this.helia + } + + async getFs (): Promise { + await this.waitForInitialization() + + if (!this.fs) { + throw new Error('UnixFS failed to initialize') + } + + return this.fs + } +} diff --git a/examples/helia-angular/src/index.html b/examples/helia-angular/src/index.html new file mode 100644 index 00000000..2516f626 --- /dev/null +++ b/examples/helia-angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Helia-Angular + + + + + + + + diff --git a/examples/helia-angular/src/main.server.ts b/examples/helia-angular/src/main.server.ts new file mode 100644 index 00000000..4f7ccb16 --- /dev/null +++ b/examples/helia-angular/src/main.server.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' +import { config } from './app/app.config.server' + +const bootstrap = async () => bootstrapApplication(AppComponent, config) + +export default bootstrap diff --git a/examples/helia-angular/src/main.ts b/examples/helia-angular/src/main.ts new file mode 100644 index 00000000..644ace3b --- /dev/null +++ b/examples/helia-angular/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' +import { appConfig } from './app/app.config' + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => { console.error(err) }) diff --git a/examples/helia-angular/src/server.ts b/examples/helia-angular/src/server.ts new file mode 100644 index 00000000..1cb83769 --- /dev/null +++ b/examples/helia-angular/src/server.ts @@ -0,0 +1,66 @@ +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { + AngularNodeAppEngine, + createNodeRequestHandler, + isMainModule, + writeResponseToNodeResponse +} from '@angular/ssr/node' +import express from 'express' + +const serverDistFolder = dirname(fileURLToPath(import.meta.url)) +const browserDistFolder = resolve(serverDistFolder, '../browser') + +const app = express() +const angularApp = new AngularNodeAppEngine() + +/** + * Example Express Rest API endpoints can be defined here. + * Uncomment and define endpoints as necessary. + * + * Example: + * ```ts + * app.get('/api/**', (req, res) => { + * // Handle API request + * }); + * ``` + */ + +/** + * Serve static files from /browser + */ +app.use( + express.static(browserDistFolder, { + maxAge: '1y', + index: false, + redirect: false + }) +) + +/** + * Handle all other requests by rendering the Angular application. + */ +app.use('/**', (req, res, next) => { + angularApp + .handle(req) + .then(async (response) => + response ? writeResponseToNodeResponse(response, res) : next() + ) + .catch(next) +}) + +/** + * Start the server if this module is the main entry point. + * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. + */ +if (isMainModule(import.meta.url)) { + const port = process.env['PORT'] || 4000 + app.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`) + }) +} + +/** + * The request handler used by the Angular CLI (dev-server and during build). + */ +export const reqHandler = createNodeRequestHandler(app) diff --git a/examples/helia-angular/src/styles.css b/examples/helia-angular/src/styles.css new file mode 100644 index 00000000..90d4ee00 --- /dev/null +++ b/examples/helia-angular/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/examples/helia-angular/tsconfig.app.json b/examples/helia-angular/tsconfig.app.json new file mode 100644 index 00000000..9ab8527b --- /dev/null +++ b/examples/helia-angular/tsconfig.app.json @@ -0,0 +1,19 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [ + "node" + ] + }, + "files": [ + "src/main.ts", + "src/main.server.ts", + "src/server.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/examples/helia-angular/tsconfig.json b/examples/helia-angular/tsconfig.json new file mode 100644 index 00000000..adf6ed78 --- /dev/null +++ b/examples/helia-angular/tsconfig.json @@ -0,0 +1,27 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ESNext", + "module": "ES2022" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/helia-angular/tsconfig.spec.json b/examples/helia-angular/tsconfig.spec.json new file mode 100644 index 00000000..5fb748d9 --- /dev/null +++ b/examples/helia-angular/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +}